@nocobase/app 3.0.0-alpha.2 → 3.0.0-alpha.4

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.
@@ -75,47 +75,31 @@ function runBrowserChecker(options: RunBrowserCheckerOptions) {
75
75
  }
76
76
 
77
77
  describe('v2 browser checker', () => {
78
- it('redirects the modern client root to Settings only in modern-only mode', () => {
79
- expect(
80
- runBrowserChecker({
81
- appClientEntryMode: 'modern-only',
82
- pathname: '/v/',
83
- }).replacements,
84
- ).toEqual(['https://example.test/settings']);
85
-
86
- expect(
87
- runBrowserChecker({
88
- appClientEntryMode: 'modern-default',
89
- pathname: '/v/',
90
- }).replacements,
91
- ).toEqual([]);
92
-
93
- expect(
94
- runBrowserChecker({
95
- pathname: '/v/',
96
- }).replacements,
97
- ).toEqual([]);
98
- });
99
-
100
- it('redirects scoped modern client roots to scoped Settings only in modern-only mode', () => {
101
- expect(
102
- runBrowserChecker({
103
- appClientEntryMode: 'modern-only',
104
- pathname: '/v/apps/demo',
105
- search: '?from=admin',
106
- hash: '#portal',
107
- }).replacements,
108
- ).toEqual(['https://example.test/settings/apps/demo?from=admin#portal']);
78
+ it.each([undefined, 'modern-default', 'modern-only'] as const)(
79
+ 'lets the modern client handle its root for entry mode %s',
80
+ (appClientEntryMode) => {
81
+ expect(
82
+ runBrowserChecker({
83
+ appClientEntryMode,
84
+ pathname: '/v/',
85
+ }).replacements,
86
+ ).toEqual([]);
87
+ },
88
+ );
109
89
 
110
- expect(
111
- runBrowserChecker({
112
- appClientEntryMode: 'modern-default',
113
- pathname: '/v/apps/demo',
114
- search: '?from=admin',
115
- hash: '#portal',
116
- }).replacements,
117
- ).toEqual([]);
118
- });
90
+ it.each([undefined, 'modern-default', 'modern-only'] as const)(
91
+ 'lets scoped modern clients handle their roots for entry mode %s',
92
+ (appClientEntryMode) => {
93
+ expect(
94
+ runBrowserChecker({
95
+ appClientEntryMode,
96
+ pathname: '/v/apps/demo',
97
+ search: '?from=admin',
98
+ hash: '#portal',
99
+ }).replacements,
100
+ ).toEqual([]);
101
+ },
102
+ );
119
103
 
120
104
  it('keeps Settings scoped trailing slash normalization independent from entry mode', () => {
121
105
  expect(
@@ -0,0 +1,34 @@
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 { describe, expect, it } from 'vitest';
11
+ import { isClientDevProxyPath, rewriteClientDevProxyRootPath } from '../clientDevProxy';
12
+
13
+ describe('client dev proxy', () => {
14
+ it.each([
15
+ ['/x', '/x/', true],
16
+ ['/x/', '/x/', true],
17
+ ['/x/test2/assets/index.js', '/x/', true],
18
+ ['/x-other', '/x/', false],
19
+ ['/console/v?from=portal', '/console/v/', true],
20
+ ['/v', '/console/v/', false],
21
+ ])('matches complete client base paths: %s', (url, basePath, expected) => {
22
+ expect(isClientDevProxyPath(url, basePath)).toBe(expected);
23
+ });
24
+
25
+ it.each([
26
+ ['/v', '/v/', '/v/'],
27
+ ['/v?from=portal', '/v/', '/v/?from=portal'],
28
+ ['/console/v#section', '/console/v/', '/console/v/#section'],
29
+ ['/v/admin', '/v/', '/v/admin'],
30
+ ['/v/', '/v/', '/v/'],
31
+ ])('normalizes only a slashless client root: %s', (url, basePath, expected) => {
32
+ expect(rewriteClientDevProxyRootPath(url, basePath)).toBe(expected);
33
+ });
34
+ });
@@ -16,6 +16,7 @@ import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
16
16
  import { pluginReact } from '@rsbuild/plugin-react';
17
17
  import { pluginSvgr } from '@rsbuild/plugin-svgr';
18
18
  import { generatePlugins, getRsbuildBrowserAlias } from '@nocobase/devtools/rsbuildConfig';
19
+ import { isClientDevProxyPath, rewriteClientDevProxyRootPath } from '../clientDevProxy';
19
20
  import { createSettingsDevProxyOptions } from '../settingsDevProxy';
20
21
 
21
22
  const __filename = fileURLToPath(import.meta.url);
@@ -259,17 +260,15 @@ export default defineConfig(({ command }) => {
259
260
  xfwd: true,
260
261
  },
261
262
  {
262
- context: v2BasePath,
263
+ context: (pathname) => isClientDevProxyPath(pathname, v2BasePath),
263
264
  target: `http://127.0.0.1:${v2Port}`,
264
265
  changeOrigin: true,
265
266
  ws: true,
266
- pathRewrite: {
267
- [`^${v2BasePath}`]: v2BasePath,
268
- },
267
+ pathRewrite: (pathname) => rewriteClientDevProxyRootPath(pathname, v2BasePath),
269
268
  xfwd: true,
270
269
  },
271
270
  {
272
- context: portalBasePath,
271
+ context: (pathname) => isClientDevProxyPath(pathname, portalBasePath),
273
272
  target: proxyTargetUrl,
274
273
  changeOrigin: true,
275
274
  ws: true,
@@ -17,46 +17,6 @@ function normalizePublicPath(value) {
17
17
  return ensureTrailingSlash(normalized);
18
18
  }
19
19
 
20
- function normalizePathname(value) {
21
- const normalized = ensureLeadingSlash(String(value || '/').trim() || '/').replace(/\/{2,}/g, '/');
22
- return normalized === '/' ? normalized : normalized.replace(/\/+$/g, '');
23
- }
24
-
25
- function resolveSettingsRootPath(publicPath, modernPrefix, pathname) {
26
- const normalizedPublicPath = normalizePublicPath(publicPath);
27
- const normalizedModernPrefix =
28
- String(modernPrefix || 'v')
29
- .trim()
30
- .replace(/^\/+|\/+$/g, '') || 'v';
31
- const modernPublicPathSuffix = `/${normalizedModernPrefix}/`;
32
- if (!normalizedPublicPath.endsWith(modernPublicPathSuffix)) {
33
- return null;
34
- }
35
-
36
- const modernRootPath = normalizePathname(normalizedPublicPath);
37
- const normalizedPathname = normalizePathname(pathname);
38
- let relativePath = '';
39
- if (normalizedPathname !== modernRootPath) {
40
- if (!normalizedPathname.startsWith(`${modernRootPath}/`)) {
41
- return null;
42
- }
43
- relativePath = normalizedPathname.slice(modernRootPath.length + 1);
44
- }
45
-
46
- let appScope = '';
47
- if (relativePath) {
48
- const match = /^(apps|_app)\/([^/]+)$/.exec(relativePath);
49
- if (!match) {
50
- return null;
51
- }
52
- appScope = `/${match[1]}/${match[2]}`;
53
- }
54
-
55
- const rootPublicPath = normalizedPublicPath.slice(0, -(normalizedModernPrefix.length + 1));
56
- const rootPrefix = rootPublicPath === '/' ? '' : rootPublicPath.replace(/\/+$/g, '');
57
- return `${rootPrefix}/settings${appScope}`;
58
- }
59
-
60
20
  function isSettingsBrowserCheckerScript(script) {
61
21
  const source = String((script && script.src) || '').split(/[?#]/)[0];
62
22
  return source.endsWith('/settings/browser-checker.js');
@@ -81,19 +41,11 @@ function resolveScopedSettingsRootPath(publicPath, pathname) {
81
41
  const basename = normalizePublicPath(window['__nocobase_public_path__'] || '/');
82
42
  const currentPath = ensureLeadingSlash(String(window.location.pathname || '/').trim() || '/').replace(/\/{2,}/g, '/');
83
43
  const basenameWithoutTrailingSlash = basename === '/' ? '/' : basename.replace(/\/+$/, '');
84
- const appClientEntryMode = window['__nocobase_app_client_entry_mode__'];
85
- const settingsRootPath =
86
- appClientEntryMode === 'modern-only'
87
- ? resolveSettingsRootPath(basename, window['__nocobase_modern_client_prefix__'], currentPath)
88
- : null;
89
44
  const scopedSettingsRootPath = isSettingsBrowserCheckerScript(document.currentScript)
90
45
  ? resolveScopedSettingsRootPath(basename, currentPath)
91
46
  : null;
92
47
 
93
- if (settingsRootPath) {
94
- const newUrl = `${window.location.origin}${settingsRootPath}${window.location.search}${window.location.hash}`;
95
- window.location.replace(newUrl);
96
- } else if (scopedSettingsRootPath) {
48
+ if (scopedSettingsRootPath) {
97
49
  const newUrl = `${window.location.origin}${scopedSettingsRootPath}${window.location.search}${window.location.hash}`;
98
50
  window.location.replace(newUrl);
99
51
  } else if (basename !== '/' && currentPath === basenameWithoutTrailingSlash) {
@@ -15,6 +15,7 @@ import { pluginNodePolyfill } from '@rsbuild/plugin-node-polyfill';
15
15
  import { pluginReact } from '@rsbuild/plugin-react';
16
16
  import { pluginSvgr } from '@rsbuild/plugin-svgr';
17
17
  import { generateV2Plugins, getRsbuildBrowserAlias } from '@nocobase/devtools/rsbuildConfig';
18
+ import { isClientDevProxyPath, rewriteClientDevProxyRootPath } from '../clientDevProxy';
18
19
  import { createSettingsDevProxyOptions, isSettingsDevPath } from '../settingsDevProxy';
19
20
 
20
21
  const __filename = fileURLToPath(import.meta.url);
@@ -300,7 +301,7 @@ export default defineConfig(({ command }) => {
300
301
  changeOrigin: true,
301
302
  },
302
303
  {
303
- context: portalBasePath,
304
+ context: (pathname) => isClientDevProxyPath(pathname, portalBasePath),
304
305
  target: proxyTargetUrl,
305
306
  changeOrigin: true,
306
307
  ws: true,
@@ -324,11 +325,12 @@ export default defineConfig(({ command }) => {
324
325
  lazyCompilation: false,
325
326
  setupMiddlewares: [
326
327
  (middlewares) => {
327
- if (appClientEntryMode !== 'modern-only') {
328
- return;
329
- }
330
-
331
328
  middlewares.unshift((req, res, next) => {
329
+ req.url = rewriteClientDevProxyRootPath(String(req.url || '/'), v2PublicPath);
330
+ if (appClientEntryMode !== 'modern-only') {
331
+ next();
332
+ return;
333
+ }
332
334
  const [rawPathname = '/', query = ''] = String(req.url || '/').split('?');
333
335
  const pathname = normalizePathname(rawPathname);
334
336
  if (
@@ -337,7 +339,7 @@ export default defineConfig(({ command }) => {
337
339
  pathname.startsWith(apiBasePath) ||
338
340
  pathname.startsWith(wsBasePath) ||
339
341
  pathname.startsWith(localStorageBasePath) ||
340
- pathname.startsWith(portalBasePath) ||
342
+ isClientDevProxyPath(pathname, portalBasePath) ||
341
343
  pathname.startsWith(staticBasePath) ||
342
344
  isSettingsDevPath(pathname, appPublicPath)
343
345
  ) {
@@ -0,0 +1,32 @@
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
+ function normalizeBasePath(value: string) {
11
+ let normalized = value || '/';
12
+ if (!normalized.startsWith('/')) {
13
+ normalized = `/${normalized}`;
14
+ }
15
+ normalized = normalized.replace(/\/{2,}/g, '/');
16
+ return normalized === '/' ? normalized : normalized.replace(/\/+$/g, '');
17
+ }
18
+
19
+ export function isClientDevProxyPath(url: string, basePath: string) {
20
+ const [pathname] = String(url || '/').split(/[?#]/, 1);
21
+ const normalizedBasePath = normalizeBasePath(basePath);
22
+ return pathname === normalizedBasePath || pathname.startsWith(`${normalizedBasePath}/`);
23
+ }
24
+
25
+ export function rewriteClientDevProxyRootPath(url: string, basePath: string) {
26
+ const normalizedBasePath = normalizeBasePath(basePath);
27
+ const [pathname] = String(url || '/').split(/[?#]/, 1);
28
+ if (pathname !== normalizedBasePath || normalizedBasePath === '/') {
29
+ return url;
30
+ }
31
+ return `${normalizedBasePath}/${url.slice(pathname.length)}`;
32
+ }