@nocobase/cli-v1 2.3.0-beta.6 → 2.4.0-alpha.2
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.
- package/nocobase.conf.tpl +90 -0
- package/package.json +4 -4
- package/src/__tests__/build-index-html.test.js +14 -0
- package/src/__tests__/client-extract.test.js +55 -0
- package/src/__tests__/create-nginx-conf.test.js +87 -0
- package/src/__tests__/dev-command.test.js +60 -3
- package/src/__tests__/util.test.js +5 -0
- package/src/commands/client.js +98 -2
- package/src/commands/create-nginx-conf.js +11 -0
- package/src/commands/dev.js +97 -3
- package/src/commands/docs.js +7 -1
- package/src/util.js +6 -2
package/nocobase.conf.tpl
CHANGED
|
@@ -128,6 +128,34 @@ server {
|
|
|
128
128
|
send_timeout 600;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
location ^~ {{settingsAssetsPath}} {
|
|
132
|
+
alias {{cwd}}/node_modules/@nocobase/app/dist/client/settings/assets/;
|
|
133
|
+
expires 365d;
|
|
134
|
+
add_header Cache-Control "public";
|
|
135
|
+
access_log off;
|
|
136
|
+
autoindex off;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# The standalone Settings SPA is a Client V2 surface. Keep this matcher
|
|
140
|
+
# narrow so legacy /admin/settings routes continue to use the v1 HTML.
|
|
141
|
+
location ~ {{settingsDocumentPattern}} {
|
|
142
|
+
proxy_pass http://127.0.0.1:{{apiPort}};
|
|
143
|
+
proxy_http_version 1.1;
|
|
144
|
+
proxy_set_header Upgrade $http_upgrade;
|
|
145
|
+
proxy_set_header Connection 'upgrade';
|
|
146
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
147
|
+
proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
|
|
148
|
+
proxy_set_header Host $final_host;
|
|
149
|
+
proxy_set_header Referer $http_referer;
|
|
150
|
+
proxy_set_header User-Agent $http_user_agent;
|
|
151
|
+
add_header Cache-Control 'no-cache, no-store';
|
|
152
|
+
proxy_cache_bypass $http_upgrade;
|
|
153
|
+
proxy_connect_timeout 600;
|
|
154
|
+
proxy_send_timeout 600;
|
|
155
|
+
proxy_read_timeout 600;
|
|
156
|
+
send_timeout 600;
|
|
157
|
+
}
|
|
158
|
+
|
|
131
159
|
# RFC 8414 root-mounted discovery compatibility for path-based issuers/resources.
|
|
132
160
|
location ~ ^/\.well-known/oauth-authorization-server/(.+)$ {
|
|
133
161
|
rewrite ^/\.well-known/oauth-authorization-server/(.+)$ /$1/.well-known/oauth-authorization-server break;
|
|
@@ -186,6 +214,68 @@ server {
|
|
|
186
214
|
|
|
187
215
|
|
|
188
216
|
|
|
217
|
+
location = {{publicPath}}{{portalClientPrefix}} {
|
|
218
|
+
absolute_redirect off;
|
|
219
|
+
return 302 {{v2PublicPath}}$is_args$args;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
location = {{publicPath}}{{portalClientPrefix}}/ {
|
|
223
|
+
absolute_redirect off;
|
|
224
|
+
return 302 {{v2PublicPath}}$is_args$args;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
location ^~ {{publicPath}}{{portalClientPrefix}}/apps/ {
|
|
228
|
+
absolute_redirect off;
|
|
229
|
+
|
|
230
|
+
if ($uri ~ ^{{publicPath}}{{portalClientPrefix}}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {
|
|
231
|
+
return 302 {{v2PublicPath}}apps/$subapp/$is_args$args;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if ($uri ~ ^{{publicPath}}{{portalClientPrefix}}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {
|
|
235
|
+
return 308 {{publicPath}}{{portalClientPrefix}}/apps/$subapp/$portal/$is_args$args;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if ($uri !~ ^{{publicPath}}{{portalClientPrefix}}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
|
|
239
|
+
return 404;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
root {{cwd}}/storage;
|
|
243
|
+
|
|
244
|
+
if ($portal_path = "") {
|
|
245
|
+
rewrite ^ /portals/$subapp/$portal/dist/index.html break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try_files
|
|
249
|
+
/portals/$subapp/$portal/dist/$portal_path
|
|
250
|
+
/portals/$subapp/$portal/dist/$portal_path/
|
|
251
|
+
/portals/$subapp/$portal/dist/index.html
|
|
252
|
+
=404;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
location ^~ {{publicPath}}{{portalClientPrefix}}/ {
|
|
256
|
+
absolute_redirect off;
|
|
257
|
+
|
|
258
|
+
if ($uri ~ ^{{publicPath}}{{portalClientPrefix}}/(?<portal>[A-Za-z0-9_-]+)$) {
|
|
259
|
+
return 308 {{publicPath}}{{portalClientPrefix}}/$portal/$is_args$args;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ($uri !~ ^{{publicPath}}{{portalClientPrefix}}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
|
|
263
|
+
return 404;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
root {{cwd}}/storage;
|
|
267
|
+
|
|
268
|
+
if ($portal_path = "") {
|
|
269
|
+
rewrite ^ /portals/main/$portal/dist/index.html break;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try_files
|
|
273
|
+
/portals/main/$portal/dist/$portal_path
|
|
274
|
+
/portals/main/$portal/dist/$portal_path/
|
|
275
|
+
/portals/main/$portal/dist/index.html
|
|
276
|
+
=404;
|
|
277
|
+
}
|
|
278
|
+
|
|
189
279
|
location {{publicPath}} {
|
|
190
280
|
alias {{cwd}}/node_modules/@nocobase/app/dist/client/;
|
|
191
281
|
try_files $uri $uri/ /index.html;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli-v1",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0-alpha.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
"nocobase-v1": "./bin/index.js"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@nocobase/cli": "2.
|
|
12
|
+
"@nocobase/cli": "2.4.0-alpha.2",
|
|
13
13
|
"@nocobase/license-kit": "^0.3.8",
|
|
14
|
-
"@nocobase/utils": "2.
|
|
14
|
+
"@nocobase/utils": "2.4.0-alpha.2",
|
|
15
15
|
"chalk": "^4.1.1",
|
|
16
16
|
"commander": "^9.2.0",
|
|
17
17
|
"deepmerge": "^4.3.1",
|
|
@@ -33,5 +33,5 @@
|
|
|
33
33
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
|
34
34
|
"directory": "packages/core/cli"
|
|
35
35
|
},
|
|
36
|
-
"gitHead": "
|
|
36
|
+
"gitHead": "706bb81047a05f39f2ddce781ebcafeea4ec9cd5"
|
|
37
37
|
}
|
|
@@ -87,6 +87,20 @@ describe('cli-v1 buildIndexHtml', () => {
|
|
|
87
87
|
fs.removeSync(appRoot);
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
test('injects settings-default as a valid client entry mode', () => {
|
|
91
|
+
const appRoot = createAppPackageRoot();
|
|
92
|
+
process.argv = ['node', 'nocobase-v1', 'start'];
|
|
93
|
+
process.env.APP_PACKAGE_ROOT = appRoot;
|
|
94
|
+
process.env.APP_PUBLIC_PATH = '/';
|
|
95
|
+
process.env.APP_CLIENT_ENTRY_MODE = 'settings-default';
|
|
96
|
+
|
|
97
|
+
buildIndexHtml();
|
|
98
|
+
|
|
99
|
+
const html = fs.readFileSync(path.join(appRoot, 'dist/client/index.html'), 'utf-8');
|
|
100
|
+
expect(html).toContain("window['__nocobase_app_client_entry_mode__'] = 'settings-default';");
|
|
101
|
+
fs.removeSync(appRoot);
|
|
102
|
+
});
|
|
103
|
+
|
|
90
104
|
test('refreshes cached tpl when new runtime placeholders are missing', () => {
|
|
91
105
|
const appRoot = createAppPackageRoot();
|
|
92
106
|
const tplPath = path.join(appRoot, 'dist/client/index.html.tpl');
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
/* eslint-env jest */
|
|
11
|
+
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { renderExtractedClientIndexHtml } = require('../commands/client');
|
|
16
|
+
|
|
17
|
+
describe('cli-v1 client:extract', () => {
|
|
18
|
+
const originalEnv = { ...process.env };
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
process.env = { ...originalEnv };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('renders extracted root index with the current client entry mode', () => {
|
|
25
|
+
const target = fs.mkdtempSync(path.join(os.tmpdir(), 'nocobase-client-extract-'));
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(target, 'index.html.tpl'),
|
|
28
|
+
[
|
|
29
|
+
'<html><head>',
|
|
30
|
+
"<script>window['__webpack_public_path__'] = '{{env.CDN_BASE_URL}}';",
|
|
31
|
+
"window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';",
|
|
32
|
+
"window['__nocobase_modern_client_prefix__'] = '{{env.APP_MODERN_CLIENT_PREFIX}}';",
|
|
33
|
+
"window['__nocobase_app_client_entry_mode__'] = '{{env.APP_CLIENT_ENTRY_MODE}}';</script>",
|
|
34
|
+
'<script src="{{env.APP_PUBLIC_PATH}}browser-checker.js?v=1"></script>',
|
|
35
|
+
'<script type="module" src="assets/runtime.js"></script>',
|
|
36
|
+
'</head><body></body></html>',
|
|
37
|
+
].join(''),
|
|
38
|
+
'utf-8',
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
process.env.APP_PUBLIC_PATH = '/';
|
|
42
|
+
process.env.APP_MODERN_CLIENT_PREFIX = 'v';
|
|
43
|
+
process.env.APP_CLIENT_ENTRY_MODE = 'modern-only';
|
|
44
|
+
delete process.env.CDN_BASE_URL;
|
|
45
|
+
|
|
46
|
+
expect(renderExtractedClientIndexHtml(target, '2.2.0-beta.15')).toBe(true);
|
|
47
|
+
|
|
48
|
+
const html = fs.readFileSync(path.join(target, 'index.html'), 'utf-8');
|
|
49
|
+
expect(html).toContain("window['__nocobase_app_client_entry_mode__'] = 'modern-only';");
|
|
50
|
+
expect(html).toContain("window['__webpack_public_path__'] = '/dist/2.2.0-beta.15/';");
|
|
51
|
+
expect(html).toContain('src="/dist/2.2.0-beta.15/browser-checker.js?v=1"');
|
|
52
|
+
expect(html).toContain('src="/dist/2.2.0-beta.15/assets/runtime.js"');
|
|
53
|
+
fs.removeSync(target);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
/* eslint-env jest */
|
|
11
|
+
|
|
12
|
+
const fs = require('fs-extra');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
|
|
16
|
+
const registerCreateNginxConf = require('../commands/create-nginx-conf');
|
|
17
|
+
|
|
18
|
+
describe('create-nginx-conf routing', () => {
|
|
19
|
+
const originalEnv = { ...process.env };
|
|
20
|
+
let storagePath;
|
|
21
|
+
|
|
22
|
+
afterEach(async () => {
|
|
23
|
+
process.env = { ...originalEnv };
|
|
24
|
+
if (storagePath) {
|
|
25
|
+
await fs.remove(storagePath);
|
|
26
|
+
storagePath = undefined;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
async function renderConfig(appPublicPath) {
|
|
31
|
+
storagePath = await fs.mkdtemp(path.join(os.tmpdir(), 'nocobase-nginx-settings-'));
|
|
32
|
+
process.env.APP_PUBLIC_PATH = appPublicPath;
|
|
33
|
+
process.env.APP_MODERN_CLIENT_PREFIX = 'v';
|
|
34
|
+
process.env.APP_PORT = '13000';
|
|
35
|
+
process.env.STORAGE_PATH = storagePath;
|
|
36
|
+
|
|
37
|
+
let action;
|
|
38
|
+
registerCreateNginxConf({
|
|
39
|
+
command() {
|
|
40
|
+
return {
|
|
41
|
+
action(callback) {
|
|
42
|
+
action = callback;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
await action();
|
|
48
|
+
return await fs.readFile(path.join(storagePath, 'nocobase.conf'), 'utf8');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test.each([
|
|
52
|
+
['root mount', '/', '/settings/assets/', '^/settings(?:/|$)'],
|
|
53
|
+
['custom public path', '/nocobase/', '/nocobase/settings/assets/', '^/nocobase/settings(?:/|$)'],
|
|
54
|
+
])(
|
|
55
|
+
'proxies Settings documents and caches Settings assets for %s',
|
|
56
|
+
async (_label, publicPath, assetsPath, routePattern) => {
|
|
57
|
+
const config = await renderConfig(publicPath);
|
|
58
|
+
|
|
59
|
+
expect(config).toContain(`location ^~ ${assetsPath} {`);
|
|
60
|
+
expect(config).toContain('dist/client/settings/assets/;');
|
|
61
|
+
expect(config).toContain('expires 365d;');
|
|
62
|
+
expect(config).toContain(`location ~ ${routePattern} {`);
|
|
63
|
+
expect(config).toContain('proxy_pass http://127.0.0.1:13000;');
|
|
64
|
+
expect(config).not.toContain('location ~ ^/admin/settings');
|
|
65
|
+
expect(config).toContain('try_files $uri $uri/ /index.html;');
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
test.each([
|
|
70
|
+
['root mount', '/', '/x', '/v/'],
|
|
71
|
+
['custom public path', '/nocobase/', '/nocobase/x', '/nocobase/v/'],
|
|
72
|
+
])('redirects Portal roots to the Modern Client for %s', async (_label, publicPath, portalRoot, modernRoot) => {
|
|
73
|
+
const config = await renderConfig(publicPath);
|
|
74
|
+
|
|
75
|
+
expect(config).toContain(`location = ${portalRoot} {
|
|
76
|
+
absolute_redirect off;
|
|
77
|
+
return 302 ${modernRoot}$is_args$args;
|
|
78
|
+
}`);
|
|
79
|
+
expect(config).toContain(`location = ${portalRoot}/ {
|
|
80
|
+
absolute_redirect off;
|
|
81
|
+
return 302 ${modernRoot}$is_args$args;
|
|
82
|
+
}`);
|
|
83
|
+
expect(config).toContain(`return 302 ${modernRoot}$is_args$args;`);
|
|
84
|
+
expect(config).toContain(`if ($uri ~ ^${portalRoot}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {`);
|
|
85
|
+
expect(config).toContain(`return 302 ${modernRoot}apps/$subapp/$is_args$args;`);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -9,13 +9,28 @@
|
|
|
9
9
|
|
|
10
10
|
/* eslint-env jest */
|
|
11
11
|
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
buildAppDevForwardArgs,
|
|
14
|
+
createSettingsDevProcessOptions,
|
|
15
|
+
forwardDevToAppDev,
|
|
16
|
+
resolveDevRuntimeMode,
|
|
17
|
+
resolveSettingsDevPort,
|
|
18
|
+
} = require('../commands/dev')._test;
|
|
13
19
|
|
|
14
20
|
describe('cli-v1 dev command', () => {
|
|
15
21
|
test('buildAppDevForwardArgs rewrites dev argv to app-dev while preserving extra args', () => {
|
|
16
22
|
expect(
|
|
17
|
-
buildAppDevForwardArgs([
|
|
18
|
-
|
|
23
|
+
buildAppDevForwardArgs([
|
|
24
|
+
'node',
|
|
25
|
+
'nocobase-v1',
|
|
26
|
+
'dev',
|
|
27
|
+
'--rsbuild',
|
|
28
|
+
'--quickstart',
|
|
29
|
+
'--port',
|
|
30
|
+
'13000',
|
|
31
|
+
'--inspect=9229',
|
|
32
|
+
]),
|
|
33
|
+
).toEqual(['app-dev', '--rsbuild', '--quickstart', '--port', '13000', '--inspect=9229']);
|
|
19
34
|
});
|
|
20
35
|
|
|
21
36
|
test('forwardDevToAppDev delegates to nocobase-v1 app-dev for create-app projects', async () => {
|
|
@@ -37,6 +52,7 @@ describe('cli-v1 dev command', () => {
|
|
|
37
52
|
useModernOnlyEntryMode: false,
|
|
38
53
|
shouldRunClient: true,
|
|
39
54
|
shouldRunClientV2: true,
|
|
55
|
+
shouldRunSettings: true,
|
|
40
56
|
shouldRunServer: true,
|
|
41
57
|
});
|
|
42
58
|
});
|
|
@@ -46,6 +62,17 @@ describe('cli-v1 dev command', () => {
|
|
|
46
62
|
useModernOnlyEntryMode: true,
|
|
47
63
|
shouldRunClient: false,
|
|
48
64
|
shouldRunClientV2: true,
|
|
65
|
+
shouldRunSettings: true,
|
|
66
|
+
shouldRunServer: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('resolveDevRuntimeMode keeps all clients in settings-default', () => {
|
|
71
|
+
expect(resolveDevRuntimeMode({ appClientEntryMode: 'settings-default' })).toMatchObject({
|
|
72
|
+
useModernOnlyEntryMode: false,
|
|
73
|
+
shouldRunClient: true,
|
|
74
|
+
shouldRunClientV2: true,
|
|
75
|
+
shouldRunSettings: true,
|
|
49
76
|
shouldRunServer: true,
|
|
50
77
|
});
|
|
51
78
|
});
|
|
@@ -55,7 +82,37 @@ describe('cli-v1 dev command', () => {
|
|
|
55
82
|
useModernOnlyEntryMode: false,
|
|
56
83
|
shouldRunClient: false,
|
|
57
84
|
shouldRunClientV2: true,
|
|
85
|
+
shouldRunSettings: true,
|
|
58
86
|
shouldRunServer: false,
|
|
59
87
|
});
|
|
60
88
|
});
|
|
89
|
+
|
|
90
|
+
test('resolveSettingsDevPort reserves APP_PORT + 3 by default', () => {
|
|
91
|
+
expect(resolveSettingsDevPort(13001)).toBe(13004);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('createSettingsDevProcessOptions uses the standalone config and settings HMR path', () => {
|
|
95
|
+
expect(
|
|
96
|
+
createSettingsDevProcessOptions({
|
|
97
|
+
appPackageRoot: '/repo/packages/core/app',
|
|
98
|
+
appPort: 13001,
|
|
99
|
+
settingsPort: 13004,
|
|
100
|
+
browserPort: 13001,
|
|
101
|
+
appPublicPath: '/nocobase/',
|
|
102
|
+
processEnv: { API_BASE_URL: '/api/' },
|
|
103
|
+
}),
|
|
104
|
+
).toMatchObject({
|
|
105
|
+
command: 'rsbuild',
|
|
106
|
+
args: ['dev', '--config', '/repo/packages/core/app/client-settings/rsbuild.config.ts'],
|
|
107
|
+
runOptions: {
|
|
108
|
+
prefix: 'client-settings',
|
|
109
|
+
env: {
|
|
110
|
+
APP_PORT: '13001',
|
|
111
|
+
APP_SETTINGS_PORT: '13004',
|
|
112
|
+
RSPACK_HMR_CLIENT_PORT: '13001',
|
|
113
|
+
RSPACK_HMR_PATH: '/nocobase/settings/__rspack_hmr',
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
});
|
|
61
118
|
});
|
|
@@ -9,9 +9,14 @@
|
|
|
9
9
|
|
|
10
10
|
/* eslint-env jest */
|
|
11
11
|
|
|
12
|
+
const { normalizeModernClientPrefix } = require('../util');
|
|
12
13
|
const { colorizedDevLogEnv, createRunWithPrefixLabel } = require('../util')._test;
|
|
13
14
|
|
|
14
15
|
describe('cli-v1 util helpers', () => {
|
|
16
|
+
test('normalizeModernClientPrefix reserves the Settings SPA path', () => {
|
|
17
|
+
expect(() => normalizeModernClientPrefix('/settings/')).toThrow('APP_MODERN_CLIENT_PREFIX "settings" is reserved');
|
|
18
|
+
});
|
|
19
|
+
|
|
15
20
|
test('colorizedDevLogEnv enables color for dev child output by default', () => {
|
|
16
21
|
expect(colorizedDevLogEnv({})).toEqual({ FORCE_COLOR: '1' });
|
|
17
22
|
});
|
package/src/commands/client.js
CHANGED
|
@@ -11,7 +11,100 @@ const chalk = require('chalk');
|
|
|
11
11
|
const fs = require('fs-extra');
|
|
12
12
|
const { resolve } = require('path');
|
|
13
13
|
const { discoverPluginPackages } = require('@nocobase/utils/plugin-package');
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
normalizeModernClientPrefix,
|
|
16
|
+
resolveAppClientEntryMode,
|
|
17
|
+
resolvePublicPath,
|
|
18
|
+
storagePathJoin,
|
|
19
|
+
} = require('../util');
|
|
20
|
+
|
|
21
|
+
function replaceRuntimeEnvPlaceholders(data, values) {
|
|
22
|
+
return data
|
|
23
|
+
.replace(/\{\{env.CDN_BASE_URL\}\}/g, values.CDN_BASE_URL)
|
|
24
|
+
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, values.APP_PUBLIC_PATH)
|
|
25
|
+
.replace(/\{\{env.APP_MODERN_CLIENT_PREFIX\}\}/g, values.APP_MODERN_CLIENT_PREFIX)
|
|
26
|
+
.replace(/\{\{env.APP_CLIENT_ENTRY_MODE\}\}/g, values.APP_CLIENT_ENTRY_MODE)
|
|
27
|
+
.replace(/\{\{env.API_CLIENT_SHARE_TOKEN\}\}/g, values.API_CLIENT_SHARE_TOKEN)
|
|
28
|
+
.replace(/\{\{env.API_CLIENT_STORAGE_TYPE\}\}/g, values.API_CLIENT_STORAGE_TYPE)
|
|
29
|
+
.replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, values.API_CLIENT_STORAGE_PREFIX)
|
|
30
|
+
.replace(/\{\{env.API_BASE_URL\}\}/g, values.API_BASE_URL)
|
|
31
|
+
.replace(/\{\{env.WS_URL\}\}/g, values.WS_URL)
|
|
32
|
+
.replace(/\{\{env.WS_PATH\}\}/g, values.WS_PATH)
|
|
33
|
+
.replace(/\{\{env.NOCOBASE_APP_DEV\}\}/g, values.NOCOBASE_APP_DEV)
|
|
34
|
+
.replace(/\{\{env.ESM_CDN_BASE_URL\}\}/g, values.ESM_CDN_BASE_URL)
|
|
35
|
+
.replace(/\{\{env.ESM_CDN_SUFFIX\}\}/g, values.ESM_CDN_SUFFIX);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function replaceRuntimeAssignment(data, key, value) {
|
|
39
|
+
const literal =
|
|
40
|
+
typeof value === 'boolean' ? String(value) : `'${String(value).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
41
|
+
const assignment = `window['${key}'] = ${literal};`;
|
|
42
|
+
const pattern = new RegExp(`window\\['${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'\\]\\s*=\\s*[^;]*;`);
|
|
43
|
+
|
|
44
|
+
return pattern.test(data) ? data.replace(pattern, assignment) : data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function renderExtractedClientIndexHtml(target, version) {
|
|
48
|
+
const indexPath = resolve(target, 'index.html');
|
|
49
|
+
const tplPath = resolve(target, 'index.html.tpl');
|
|
50
|
+
if (!fs.existsSync(indexPath) && !fs.existsSync(tplPath)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const appPublicPath = resolvePublicPath(process.env.APP_PUBLIC_PATH || '/');
|
|
55
|
+
const cdnBaseUrl =
|
|
56
|
+
process.env.CDN_BASE_URL || `${appPublicPath.replace(/\/$/, '')}/dist/${version}/`.replace(/^\/\//, '/');
|
|
57
|
+
const values = {
|
|
58
|
+
CDN_BASE_URL: cdnBaseUrl,
|
|
59
|
+
APP_PUBLIC_PATH: appPublicPath,
|
|
60
|
+
APP_MODERN_CLIENT_PREFIX: normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX),
|
|
61
|
+
APP_CLIENT_ENTRY_MODE: resolveAppClientEntryMode(),
|
|
62
|
+
API_CLIENT_SHARE_TOKEN: process.env.API_CLIENT_SHARE_TOKEN || 'false',
|
|
63
|
+
API_CLIENT_STORAGE_TYPE: process.env.API_CLIENT_STORAGE_TYPE || 'localStorage',
|
|
64
|
+
API_CLIENT_STORAGE_PREFIX: process.env.API_CLIENT_STORAGE_PREFIX || 'NOCOBASE_',
|
|
65
|
+
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH || '/api/',
|
|
66
|
+
WS_URL: process.env.WEBSOCKET_URL || '',
|
|
67
|
+
WS_PATH: process.env.WS_PATH || '/ws',
|
|
68
|
+
NOCOBASE_APP_DEV: 'false',
|
|
69
|
+
ESM_CDN_BASE_URL: process.env.ESM_CDN_BASE_URL || 'https://esm.sh',
|
|
70
|
+
ESM_CDN_SUFFIX: process.env.ESM_CDN_SUFFIX || '',
|
|
71
|
+
};
|
|
72
|
+
const sourcePath = fs.existsSync(tplPath) ? tplPath : indexPath;
|
|
73
|
+
let data = replaceRuntimeEnvPlaceholders(fs.readFileSync(sourcePath, 'utf-8'), values)
|
|
74
|
+
.replace(/((?:src|href)=")(?:\.\/)?assets\//g, `$1${values.APP_PUBLIC_PATH}assets/`)
|
|
75
|
+
.replace(/((?:src|href)=")\/assets\//g, `$1${values.APP_PUBLIC_PATH}assets/`)
|
|
76
|
+
.replace('src="/umi.', `src="${values.APP_PUBLIC_PATH}umi.`)
|
|
77
|
+
.replace(/((?:src|href)="[^"]*?)\/{2,}(assets\/)/g, '$1/$2');
|
|
78
|
+
|
|
79
|
+
if (values.CDN_BASE_URL) {
|
|
80
|
+
const appBaseUrl = values.CDN_BASE_URL.replace(/\/+$/, '');
|
|
81
|
+
const publicPath = values.APP_PUBLIC_PATH.replace(/\/+$/, '');
|
|
82
|
+
data = data
|
|
83
|
+
.replace(new RegExp(`src="${publicPath}/`, 'g'), `src="${appBaseUrl}/`)
|
|
84
|
+
.replace(new RegExp(`href="${publicPath}/`, 'g'), `href="${appBaseUrl}/`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
data = replaceRuntimeAssignment(data, '__webpack_public_path__', values.CDN_BASE_URL);
|
|
88
|
+
data = replaceRuntimeAssignment(data, '__nocobase_public_path__', values.APP_PUBLIC_PATH);
|
|
89
|
+
data = replaceRuntimeAssignment(data, '__nocobase_modern_client_prefix__', values.APP_MODERN_CLIENT_PREFIX);
|
|
90
|
+
data = replaceRuntimeAssignment(data, '__nocobase_app_client_entry_mode__', values.APP_CLIENT_ENTRY_MODE);
|
|
91
|
+
data = replaceRuntimeAssignment(data, '__nocobase_api_base_url__', values.API_BASE_URL);
|
|
92
|
+
data = replaceRuntimeAssignment(data, '__nocobase_api_client_storage_prefix__', values.API_CLIENT_STORAGE_PREFIX);
|
|
93
|
+
data = replaceRuntimeAssignment(data, '__nocobase_api_client_storage_type__', values.API_CLIENT_STORAGE_TYPE);
|
|
94
|
+
data = replaceRuntimeAssignment(
|
|
95
|
+
data,
|
|
96
|
+
'__nocobase_api_client_share_token__',
|
|
97
|
+
/^true$/i.test(values.API_CLIENT_SHARE_TOKEN),
|
|
98
|
+
);
|
|
99
|
+
data = replaceRuntimeAssignment(data, '__nocobase_ws_url__', values.WS_URL);
|
|
100
|
+
data = replaceRuntimeAssignment(data, '__nocobase_ws_path__', values.WS_PATH);
|
|
101
|
+
data = replaceRuntimeAssignment(data, '__nocobase_app_dev__', false);
|
|
102
|
+
data = replaceRuntimeAssignment(data, '__esm_cdn_base_url__', values.ESM_CDN_BASE_URL);
|
|
103
|
+
data = replaceRuntimeAssignment(data, '__esm_cdn_suffix__', values.ESM_CDN_SUFFIX);
|
|
104
|
+
|
|
105
|
+
fs.writeFileSync(indexPath, data, 'utf-8');
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
15
108
|
|
|
16
109
|
/**
|
|
17
110
|
* 复制主应用客户端文件
|
|
@@ -127,6 +220,7 @@ module.exports = (cli) => {
|
|
|
127
220
|
nodeModulesPath: resolve(process.cwd(), 'node_modules'),
|
|
128
221
|
});
|
|
129
222
|
const copiedMainClient = await copyMainClient(mainClientSource, target);
|
|
223
|
+
const renderedMainIndex = copiedMainClient ? renderExtractedClientIndexHtml(target, version) : false;
|
|
130
224
|
const copiedPluginBundles = await copyPluginClients(plugins, target);
|
|
131
225
|
const activeVersionFile = await writeActiveVersion(version);
|
|
132
226
|
|
|
@@ -145,7 +239,7 @@ module.exports = (cli) => {
|
|
|
145
239
|
chalk.green(
|
|
146
240
|
`Extracted client assets ${version} to ${target} (main client: ${
|
|
147
241
|
copiedMainClient ? 'yes' : 'no'
|
|
148
|
-
}, plugin bundles: ${copiedPluginBundles}).`,
|
|
242
|
+
}, rendered index: ${renderedMainIndex ? 'yes' : 'no'}, plugin bundles: ${copiedPluginBundles}).`,
|
|
149
243
|
),
|
|
150
244
|
);
|
|
151
245
|
});
|
|
@@ -198,3 +292,5 @@ module.exports = (cli) => {
|
|
|
198
292
|
}
|
|
199
293
|
});
|
|
200
294
|
};
|
|
295
|
+
|
|
296
|
+
module.exports.renderExtractedClientIndexHtml = renderExtractedClientIndexHtml;
|
|
@@ -11,6 +11,12 @@ const { resolve, posix } = require('path');
|
|
|
11
11
|
const { storagePathJoin, resolvePublicPath, resolveV2PublicPath, normalizeModernClientPrefix } = require('../util');
|
|
12
12
|
const { readFileSync, writeFileSync } = require('fs');
|
|
13
13
|
|
|
14
|
+
const PORTAL_CLIENT_PREFIX = 'x';
|
|
15
|
+
|
|
16
|
+
function escapeRegExp(value) {
|
|
17
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
*
|
|
16
22
|
* @param {Command} cli
|
|
@@ -24,6 +30,8 @@ module.exports = (cli) => {
|
|
|
24
30
|
const modernClientPrefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
|
25
31
|
const appPublicPathWithoutTrailingSlash = appPublicPath.replace(/\/$/, '');
|
|
26
32
|
const v2PublicPathWithoutTrailingSlash = v2PublicPath.replace(/\/$/, '');
|
|
33
|
+
const settingsAssetsPath = `${appPublicPath}settings/assets/`;
|
|
34
|
+
const settingsDocumentPattern = `^${escapeRegExp(appPublicPath)}settings(?:/|$)`;
|
|
27
35
|
const file = resolve(__dirname, '../../nocobase.conf.tpl');
|
|
28
36
|
const data = readFileSync(file, 'utf-8');
|
|
29
37
|
let otherLocation = '';
|
|
@@ -64,6 +72,9 @@ module.exports = (cli) => {
|
|
|
64
72
|
.replace(/\{\{distPath\}\}/g, distPath)
|
|
65
73
|
.replace(/\{\{v2PublicPath\}\}/g, v2PublicPath)
|
|
66
74
|
.replace(/\{\{v2PublicPathNoTrailingSlash\}\}/g, v2PublicPathWithoutTrailingSlash)
|
|
75
|
+
.replace(/\{\{portalClientPrefix\}\}/g, PORTAL_CLIENT_PREFIX)
|
|
76
|
+
.replace(/\{\{settingsAssetsPath\}\}/g, settingsAssetsPath)
|
|
77
|
+
.replace(/\{\{settingsDocumentPattern\}\}/g, settingsDocumentPattern)
|
|
67
78
|
.replace(/\{\{apiPort\}\}/g, process.env.APP_PORT)
|
|
68
79
|
.replace(/\{\{otherLocation\}\}/g, otherLocation);
|
|
69
80
|
const targetFile = storagePathJoin('nocobase.conf');
|
package/src/commands/dev.js
CHANGED
|
@@ -48,6 +48,48 @@ function buildAppDevForwardArgs(argv = process.argv) {
|
|
|
48
48
|
return ['app-dev', ...argv.slice(3)];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function resolveSettingsDevPort(appPort) {
|
|
52
|
+
return Number(appPort) + 3;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizePublicPath(value) {
|
|
56
|
+
let normalized = value || '/';
|
|
57
|
+
if (!normalized.startsWith('/')) {
|
|
58
|
+
normalized = `/${normalized}`;
|
|
59
|
+
}
|
|
60
|
+
if (!normalized.endsWith('/')) {
|
|
61
|
+
normalized = `${normalized}/`;
|
|
62
|
+
}
|
|
63
|
+
return normalized.replace(/\/{2,}/g, '/');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createSettingsDevProcessOptions({
|
|
67
|
+
appPackageRoot,
|
|
68
|
+
appPort,
|
|
69
|
+
settingsPort,
|
|
70
|
+
browserPort,
|
|
71
|
+
appPublicPath,
|
|
72
|
+
processEnv = process.env,
|
|
73
|
+
}) {
|
|
74
|
+
const settingsHmrPath = `${normalizePublicPath(appPublicPath)}settings/__rspack_hmr`;
|
|
75
|
+
return {
|
|
76
|
+
command: 'rsbuild',
|
|
77
|
+
args: ['dev', '--config', `${appPackageRoot}/client-settings/rsbuild.config.ts`],
|
|
78
|
+
runOptions: {
|
|
79
|
+
prefix: 'client-settings',
|
|
80
|
+
color: 'yellow',
|
|
81
|
+
env: {
|
|
82
|
+
...processEnv,
|
|
83
|
+
APP_PORT: `${appPort}`,
|
|
84
|
+
APP_SETTINGS_PORT: `${settingsPort}`,
|
|
85
|
+
NODE_ENV: 'development',
|
|
86
|
+
RSPACK_HMR_CLIENT_PORT: `${browserPort}`,
|
|
87
|
+
RSPACK_HMR_PATH: settingsHmrPath,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
51
93
|
function resolveDevRuntimeMode(opts = {}) {
|
|
52
94
|
const appClientEntryMode = opts.appClientEntryMode || resolveAppClientEntryMode();
|
|
53
95
|
const useModernOnlyEntryMode = appClientEntryMode === 'modern-only';
|
|
@@ -57,6 +99,7 @@ function resolveDevRuntimeMode(opts = {}) {
|
|
|
57
99
|
const shouldRunClientV2 = clientV2Only || useModernOnlyEntryMode || forceClient || !forceServer;
|
|
58
100
|
const shouldRunClient = !clientV2Only && !useModernOnlyEntryMode && (forceClient || !forceServer);
|
|
59
101
|
const shouldRunServer = !clientV2Only && (forceServer || !forceClient || useModernOnlyEntryMode);
|
|
102
|
+
const shouldRunSettings = shouldRunClientV2;
|
|
60
103
|
|
|
61
104
|
return {
|
|
62
105
|
appClientEntryMode,
|
|
@@ -64,6 +107,7 @@ function resolveDevRuntimeMode(opts = {}) {
|
|
|
64
107
|
shouldRunClientV2,
|
|
65
108
|
shouldRunClient,
|
|
66
109
|
shouldRunServer,
|
|
110
|
+
shouldRunSettings,
|
|
67
111
|
};
|
|
68
112
|
}
|
|
69
113
|
|
|
@@ -83,6 +127,7 @@ module.exports = (cli) => {
|
|
|
83
127
|
.option('-c, --client')
|
|
84
128
|
.option('-s, --server')
|
|
85
129
|
.option('--db-sync')
|
|
130
|
+
.option('--quickstart')
|
|
86
131
|
.option('--rsbuild')
|
|
87
132
|
.option('--client-v2-only')
|
|
88
133
|
.option('-i, --inspect [port]')
|
|
@@ -123,13 +168,18 @@ module.exports = (cli) => {
|
|
|
123
168
|
let clientPort = APP_PORT;
|
|
124
169
|
let serverPort;
|
|
125
170
|
let clientV2Port = APP_PORT;
|
|
171
|
+
let settingsPort = resolveSettingsDevPort(APP_PORT);
|
|
126
172
|
|
|
127
173
|
nodeCheck();
|
|
128
174
|
await postCheck(opts);
|
|
129
175
|
|
|
130
|
-
const {
|
|
131
|
-
|
|
132
|
-
|
|
176
|
+
const {
|
|
177
|
+
useModernOnlyEntryMode,
|
|
178
|
+
shouldRunClientV2,
|
|
179
|
+
shouldRunClient,
|
|
180
|
+
shouldRunServer,
|
|
181
|
+
shouldRunSettings,
|
|
182
|
+
} = resolveDevRuntimeMode(opts);
|
|
133
183
|
const shouldRunClientWithRsbuild = shouldRunClient && !!rsbuild;
|
|
134
184
|
|
|
135
185
|
if (shouldRunServer && server) {
|
|
@@ -150,8 +200,15 @@ module.exports = (cli) => {
|
|
|
150
200
|
clientV2Port = APP_PORT;
|
|
151
201
|
}
|
|
152
202
|
|
|
203
|
+
if (shouldRunSettings) {
|
|
204
|
+
settingsPort = await getPortPromise({
|
|
205
|
+
port: resolveSettingsDevPort(APP_PORT),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
153
209
|
let subprocessClient;
|
|
154
210
|
let subprocessClientV2;
|
|
211
|
+
let subprocessSettings;
|
|
155
212
|
|
|
156
213
|
const runDevClientV2 = () => {
|
|
157
214
|
console.log('starting client-v2', 1 * clientV2Port);
|
|
@@ -164,6 +221,7 @@ module.exports = (cli) => {
|
|
|
164
221
|
env: {
|
|
165
222
|
...process.env,
|
|
166
223
|
APP_V2_PORT: `${clientV2Port}`,
|
|
224
|
+
APP_SETTINGS_PORT: `${settingsPort}`,
|
|
167
225
|
NODE_ENV: 'development',
|
|
168
226
|
RSPACK_HMR_CLIENT_PORT: `${clientV2Only ? clientV2Port : clientPort}`,
|
|
169
227
|
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
|
@@ -181,8 +239,34 @@ module.exports = (cli) => {
|
|
|
181
239
|
);
|
|
182
240
|
};
|
|
183
241
|
|
|
242
|
+
const runDevSettings = () => {
|
|
243
|
+
console.log('starting client-settings', 1 * settingsPort);
|
|
244
|
+
const { command, args, runOptions } = createSettingsDevProcessOptions({
|
|
245
|
+
appPackageRoot: APP_PACKAGE_ROOT,
|
|
246
|
+
appPort: APP_PORT,
|
|
247
|
+
settingsPort,
|
|
248
|
+
browserPort: clientV2Only ? clientV2Port : clientPort,
|
|
249
|
+
appPublicPath: process.env.APP_PUBLIC_PATH,
|
|
250
|
+
processEnv: {
|
|
251
|
+
...process.env,
|
|
252
|
+
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
|
253
|
+
API_CLIENT_STORAGE_PREFIX: process.env.API_CLIENT_STORAGE_PREFIX,
|
|
254
|
+
API_CLIENT_STORAGE_TYPE: process.env.API_CLIENT_STORAGE_TYPE,
|
|
255
|
+
API_CLIENT_SHARE_TOKEN: process.env.API_CLIENT_SHARE_TOKEN || 'false',
|
|
256
|
+
WEBSOCKET_URL: process.env.WEBSOCKET_URL || buildWSURL(process.env.API_BASE_URL, serverPort),
|
|
257
|
+
WS_PATH: process.env.WS_PATH,
|
|
258
|
+
ESM_CDN_BASE_URL: process.env.ESM_CDN_BASE_URL || 'https://esm.sh',
|
|
259
|
+
ESM_CDN_SUFFIX: process.env.ESM_CDN_SUFFIX || '',
|
|
260
|
+
PROXY_TARGET_URL:
|
|
261
|
+
process.env.PROXY_TARGET_URL || (serverPort ? `http://127.0.0.1:${serverPort}` : undefined),
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
subprocessSettings = runWithPrefix(command, args, runOptions);
|
|
265
|
+
};
|
|
266
|
+
|
|
184
267
|
if (clientV2Only) {
|
|
185
268
|
runDevClientV2();
|
|
269
|
+
runDevSettings();
|
|
186
270
|
return;
|
|
187
271
|
}
|
|
188
272
|
|
|
@@ -203,6 +287,7 @@ module.exports = (cli) => {
|
|
|
203
287
|
APP_PORT: `${clientPort}`,
|
|
204
288
|
APP_ROOT: `${APP_PACKAGE_ROOT}/client`,
|
|
205
289
|
APP_V2_PORT: `${clientV2Port}`,
|
|
290
|
+
APP_SETTINGS_PORT: `${settingsPort}`,
|
|
206
291
|
NODE_ENV: 'development',
|
|
207
292
|
RSPACK_HMR_CLIENT_PORT: `${clientPort}`,
|
|
208
293
|
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
|
@@ -255,6 +340,9 @@ module.exports = (cli) => {
|
|
|
255
340
|
if (shouldRunClientV2) {
|
|
256
341
|
await restartSubprocess(subprocessClientV2, clientV2Port, runDevClientV2);
|
|
257
342
|
}
|
|
343
|
+
if (shouldRunSettings) {
|
|
344
|
+
await restartSubprocess(subprocessSettings, settingsPort, runDevSettings);
|
|
345
|
+
}
|
|
258
346
|
await fs.promises.writeFile(process.env.WATCH_FILE, `export const watchId = '${uid()}';`, 'utf-8');
|
|
259
347
|
}, 500);
|
|
260
348
|
|
|
@@ -324,11 +412,17 @@ module.exports = (cli) => {
|
|
|
324
412
|
if (shouldRunClientV2) {
|
|
325
413
|
runDevClientV2();
|
|
326
414
|
}
|
|
415
|
+
|
|
416
|
+
if (shouldRunSettings) {
|
|
417
|
+
runDevSettings();
|
|
418
|
+
}
|
|
327
419
|
});
|
|
328
420
|
};
|
|
329
421
|
|
|
330
422
|
module.exports._test = {
|
|
331
423
|
buildAppDevForwardArgs,
|
|
424
|
+
createSettingsDevProcessOptions,
|
|
332
425
|
forwardDevToAppDev,
|
|
333
426
|
resolveDevRuntimeMode,
|
|
427
|
+
resolveSettingsDevPort,
|
|
334
428
|
};
|
package/src/commands/docs.js
CHANGED
|
@@ -57,6 +57,8 @@ function createCdnClient() {
|
|
|
57
57
|
* @param {string} timestampDir
|
|
58
58
|
*/
|
|
59
59
|
const REWRITE_RULES = [
|
|
60
|
+
// AI API 请求保持原路径,避免被后续规则补上文档版本的时间戳前缀
|
|
61
|
+
{ sourceUrl: '^/api/ai/(.*)$', targetTemplate: () => '/api/ai/$1', flag: 'break' },
|
|
60
62
|
// /en/ 下无后缀的页面路由,去掉 /en/ 前缀并改写到目录式 index.html
|
|
61
63
|
{ sourceUrl: '^/en/([^.]*[^/.])$', targetTemplate: (ts) => `/${ts}/$1/index.html`, flag: 'break' },
|
|
62
64
|
// 其他语言和默认语言的无后缀页面路由,统一改写到目录式 index.html
|
|
@@ -147,7 +149,11 @@ async function waitForRewriteRule(cdnClient, domain, timestampDir) {
|
|
|
147
149
|
if (!config) return false;
|
|
148
150
|
const args = config.functionArgs?.functionArg || [];
|
|
149
151
|
const targetArg = args.find((a) => a.argName === 'target_url');
|
|
150
|
-
|
|
152
|
+
const expectedTarget = rule.targetTemplate(timestampDir);
|
|
153
|
+
const targetMatches = expectedTarget.startsWith(expectedPrefix)
|
|
154
|
+
? (targetArg?.argValue || '').startsWith(expectedPrefix)
|
|
155
|
+
: targetArg?.argValue === expectedTarget;
|
|
156
|
+
return config.status === 'success' && targetMatches;
|
|
151
157
|
});
|
|
152
158
|
|
|
153
159
|
if (allEffective) {
|
package/src/util.js
CHANGED
|
@@ -433,7 +433,7 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
|
433
433
|
exports.DEFAULT_MODERN_CLIENT_PREFIX = DEFAULT_MODERN_CLIENT_PREFIX;
|
|
434
434
|
|
|
435
435
|
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
|
436
|
-
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
|
436
|
+
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only', 'settings-default']);
|
|
437
437
|
|
|
438
438
|
exports.DEFAULT_APP_CLIENT_ENTRY_MODE = DEFAULT_APP_CLIENT_ENTRY_MODE;
|
|
439
439
|
|
|
@@ -462,7 +462,11 @@ function normalizeModernClientPrefix(value) {
|
|
|
462
462
|
const segment = String(value || '')
|
|
463
463
|
.trim()
|
|
464
464
|
.replace(/^\/+|\/+$/g, '');
|
|
465
|
-
|
|
465
|
+
const normalized = segment || DEFAULT_MODERN_CLIENT_PREFIX;
|
|
466
|
+
if (normalized === 'settings') {
|
|
467
|
+
throw new Error('APP_MODERN_CLIENT_PREFIX "settings" is reserved for the standalone Settings application.');
|
|
468
|
+
}
|
|
469
|
+
return normalized;
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
exports.normalizeModernClientPrefix = normalizeModernClientPrefix;
|