@nocobase/cli-v1 2.2.0-beta.9 → 3.0.0-alpha.1
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 +98 -1
- package/package.json +4 -4
- package/src/__tests__/build-index-html.test.js +34 -0
- package/src/__tests__/client-extract.test.js +55 -0
- package/src/__tests__/create-nginx-conf.test.js +68 -0
- package/src/__tests__/dev-command.test.js +77 -3
- package/src/__tests__/util.test.js +5 -0
- package/src/commands/client.js +98 -2
- package/src/commands/create-nginx-conf.js +23 -0
- package/src/commands/dev.js +123 -4
- package/src/util.js +49 -1
package/nocobase.conf.tpl
CHANGED
|
@@ -36,10 +36,11 @@ server {
|
|
|
36
36
|
gzip on;
|
|
37
37
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
38
38
|
|
|
39
|
-
location ~* ^{{publicPath}}storage/uploads/(.*\.(?:htm|html|svg|svgz|xhtml))$ {
|
|
39
|
+
location ~* ^{{publicPath}}storage/uploads/(.*\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
|
|
40
40
|
alias {{cwd}}/storage/uploads/$1;
|
|
41
41
|
add_header Cache-Control "public";
|
|
42
42
|
add_header Content-Disposition "attachment" always;
|
|
43
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
43
44
|
add_header X-Content-Type-Options "nosniff" always;
|
|
44
45
|
access_log off;
|
|
45
46
|
autoindex off;
|
|
@@ -48,6 +49,7 @@ server {
|
|
|
48
49
|
location {{publicPath}}storage/uploads/ {
|
|
49
50
|
alias {{cwd}}/storage/uploads/;
|
|
50
51
|
add_header Cache-Control "public";
|
|
52
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
51
53
|
add_header X-Content-Type-Options "nosniff" always;
|
|
52
54
|
access_log off;
|
|
53
55
|
autoindex off;
|
|
@@ -55,6 +57,8 @@ server {
|
|
|
55
57
|
location ~* \.md$ {
|
|
56
58
|
default_type text/markdown;
|
|
57
59
|
add_header Content-Disposition "inline";
|
|
60
|
+
add_header Content-Security-Policy "sandbox" always;
|
|
61
|
+
add_header X-Content-Type-Options "nosniff" always;
|
|
58
62
|
}
|
|
59
63
|
}
|
|
60
64
|
|
|
@@ -124,6 +128,34 @@ server {
|
|
|
124
128
|
send_timeout 600;
|
|
125
129
|
}
|
|
126
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
|
+
|
|
127
159
|
# RFC 8414 root-mounted discovery compatibility for path-based issuers/resources.
|
|
128
160
|
location ~ ^/\.well-known/oauth-authorization-server/(.+)$ {
|
|
129
161
|
rewrite ^/\.well-known/oauth-authorization-server/(.+)$ /$1/.well-known/oauth-authorization-server break;
|
|
@@ -163,7 +195,72 @@ server {
|
|
|
163
195
|
send_timeout 600;
|
|
164
196
|
}
|
|
165
197
|
|
|
198
|
+
# File access URLs are application routes. Keep them above the SPA
|
|
199
|
+
# locations so Nginx forwards them to the NocoBase gateway.
|
|
200
|
+
location ^~ {{publicPath}}files/ {
|
|
201
|
+
proxy_pass http://127.0.0.1:{{apiPort}};
|
|
202
|
+
proxy_http_version 1.1;
|
|
203
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
204
|
+
proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
|
|
205
|
+
proxy_set_header Host $final_host;
|
|
206
|
+
proxy_set_header Referer $http_referer;
|
|
207
|
+
proxy_set_header User-Agent $http_user_agent;
|
|
208
|
+
add_header Cache-Control 'no-cache, no-store';
|
|
209
|
+
proxy_connect_timeout 600;
|
|
210
|
+
proxy_send_timeout 600;
|
|
211
|
+
proxy_read_timeout 600;
|
|
212
|
+
send_timeout 600;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
location ^~ {{publicPath}}x/apps/ {
|
|
218
|
+
absolute_redirect off;
|
|
219
|
+
|
|
220
|
+
if ($uri ~ ^{{publicPath}}x/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {
|
|
221
|
+
return 308 {{publicPath}}x/apps/$subapp/$portal/$is_args$args;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if ($uri !~ ^{{publicPath}}x/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
|
|
225
|
+
return 404;
|
|
226
|
+
}
|
|
166
227
|
|
|
228
|
+
root {{cwd}}/storage;
|
|
229
|
+
|
|
230
|
+
if ($portal_path = "") {
|
|
231
|
+
rewrite ^ /portals/$subapp/$portal/dist/index.html break;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try_files
|
|
235
|
+
/portals/$subapp/$portal/dist/$portal_path
|
|
236
|
+
/portals/$subapp/$portal/dist/$portal_path/
|
|
237
|
+
/portals/$subapp/$portal/dist/index.html
|
|
238
|
+
=404;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
location ^~ {{publicPath}}x/ {
|
|
242
|
+
absolute_redirect off;
|
|
243
|
+
|
|
244
|
+
if ($uri ~ ^{{publicPath}}x/(?<portal>[A-Za-z0-9_-]+)$) {
|
|
245
|
+
return 308 {{publicPath}}x/$portal/$is_args$args;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if ($uri !~ ^{{publicPath}}x/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
|
|
249
|
+
return 404;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
root {{cwd}}/storage;
|
|
253
|
+
|
|
254
|
+
if ($portal_path = "") {
|
|
255
|
+
rewrite ^ /portals/main/$portal/dist/index.html break;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
try_files
|
|
259
|
+
/portals/main/$portal/dist/$portal_path
|
|
260
|
+
/portals/main/$portal/dist/$portal_path/
|
|
261
|
+
/portals/main/$portal/dist/index.html
|
|
262
|
+
=404;
|
|
263
|
+
}
|
|
167
264
|
|
|
168
265
|
location {{publicPath}} {
|
|
169
266
|
alias {{cwd}}/node_modules/@nocobase/app/dist/client/;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli-v1",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-alpha.1",
|
|
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": "
|
|
12
|
+
"@nocobase/cli": "3.0.0-alpha.1",
|
|
13
13
|
"@nocobase/license-kit": "^0.3.8",
|
|
14
|
-
"@nocobase/utils": "
|
|
14
|
+
"@nocobase/utils": "3.0.0-alpha.1",
|
|
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": "22d8be8ece179cfa5c8a4ebb2c896c92bd418e03"
|
|
37
37
|
}
|
|
@@ -24,6 +24,8 @@ function createAppPackageRoot() {
|
|
|
24
24
|
[
|
|
25
25
|
"window['__nocobase_app_dev__'] = {{env.NOCOBASE_APP_DEV}};",
|
|
26
26
|
"window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';",
|
|
27
|
+
"window['__nocobase_modern_client_prefix__'] = '{{env.APP_MODERN_CLIENT_PREFIX}}';",
|
|
28
|
+
"window['__nocobase_app_client_entry_mode__'] = '{{env.APP_CLIENT_ENTRY_MODE}}';",
|
|
27
29
|
].join('\n'),
|
|
28
30
|
'utf-8',
|
|
29
31
|
);
|
|
@@ -73,11 +75,43 @@ describe('cli-v1 buildIndexHtml', () => {
|
|
|
73
75
|
process.env.APP_PACKAGE_ROOT = appRoot;
|
|
74
76
|
process.env.APP_PUBLIC_PATH = '/';
|
|
75
77
|
process.env.NOCOBASE_APP_DEV = '';
|
|
78
|
+
process.env.APP_MODERN_CLIENT_PREFIX = 'console';
|
|
79
|
+
process.env.APP_CLIENT_ENTRY_MODE = 'modern-default';
|
|
76
80
|
|
|
77
81
|
buildIndexHtml();
|
|
78
82
|
|
|
79
83
|
const html = fs.readFileSync(path.join(appRoot, 'dist/client/index.html'), 'utf-8');
|
|
80
84
|
expect(html).toContain("window['__nocobase_app_dev__'] = false;");
|
|
85
|
+
expect(html).toContain("window['__nocobase_modern_client_prefix__'] = 'console';");
|
|
86
|
+
expect(html).toContain("window['__nocobase_app_client_entry_mode__'] = 'modern-default';");
|
|
87
|
+
fs.removeSync(appRoot);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('refreshes cached tpl when new runtime placeholders are missing', () => {
|
|
91
|
+
const appRoot = createAppPackageRoot();
|
|
92
|
+
const tplPath = path.join(appRoot, 'dist/client/index.html.tpl');
|
|
93
|
+
const indexPath = path.join(appRoot, 'dist/client/index.html');
|
|
94
|
+
fs.writeFileSync(tplPath, "window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';", 'utf-8');
|
|
95
|
+
fs.writeFileSync(
|
|
96
|
+
indexPath,
|
|
97
|
+
[
|
|
98
|
+
"window['__nocobase_public_path__'] = '{{env.APP_PUBLIC_PATH}}';",
|
|
99
|
+
"window['__nocobase_modern_client_prefix__'] = '{{env.APP_MODERN_CLIENT_PREFIX}}';",
|
|
100
|
+
"window['__nocobase_app_client_entry_mode__'] = '{{env.APP_CLIENT_ENTRY_MODE}}';",
|
|
101
|
+
].join('\n'),
|
|
102
|
+
'utf-8',
|
|
103
|
+
);
|
|
104
|
+
process.argv = ['node', 'nocobase-v1', 'start'];
|
|
105
|
+
process.env.APP_PACKAGE_ROOT = appRoot;
|
|
106
|
+
process.env.APP_PUBLIC_PATH = '/';
|
|
107
|
+
process.env.APP_MODERN_CLIENT_PREFIX = 'v';
|
|
108
|
+
process.env.APP_CLIENT_ENTRY_MODE = 'modern-only';
|
|
109
|
+
|
|
110
|
+
buildIndexHtml();
|
|
111
|
+
|
|
112
|
+
const tpl = fs.readFileSync(tplPath, 'utf-8');
|
|
113
|
+
expect(tpl).toContain('__nocobase_modern_client_prefix__');
|
|
114
|
+
expect(tpl).toContain('__nocobase_app_client_entry_mode__');
|
|
81
115
|
fs.removeSync(appRoot);
|
|
82
116
|
});
|
|
83
117
|
});
|
|
@@ -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,68 @@
|
|
|
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 Settings SPA 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
|
+
});
|
|
@@ -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 () => {
|
|
@@ -31,4 +46,63 @@ describe('cli-v1 dev command', () => {
|
|
|
31
46
|
|
|
32
47
|
expect(calls).toEqual([['nocobase-v1', ['app-dev', '--port', '13000', '--db-sync']]]);
|
|
33
48
|
});
|
|
49
|
+
|
|
50
|
+
test('resolveDevRuntimeMode keeps both clients in legacy-default', () => {
|
|
51
|
+
expect(resolveDevRuntimeMode({ appClientEntryMode: 'legacy-default' })).toMatchObject({
|
|
52
|
+
useModernOnlyEntryMode: false,
|
|
53
|
+
shouldRunClient: true,
|
|
54
|
+
shouldRunClientV2: true,
|
|
55
|
+
shouldRunSettings: true,
|
|
56
|
+
shouldRunServer: true,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('resolveDevRuntimeMode only runs v2 client and server in modern-only', () => {
|
|
61
|
+
expect(resolveDevRuntimeMode({ appClientEntryMode: 'modern-only' })).toMatchObject({
|
|
62
|
+
useModernOnlyEntryMode: true,
|
|
63
|
+
shouldRunClient: false,
|
|
64
|
+
shouldRunClientV2: true,
|
|
65
|
+
shouldRunSettings: true,
|
|
66
|
+
shouldRunServer: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('resolveDevRuntimeMode preserves explicit client-v2-only flag behavior', () => {
|
|
71
|
+
expect(resolveDevRuntimeMode({ clientV2Only: true, appClientEntryMode: 'legacy-default' })).toMatchObject({
|
|
72
|
+
useModernOnlyEntryMode: false,
|
|
73
|
+
shouldRunClient: false,
|
|
74
|
+
shouldRunClientV2: true,
|
|
75
|
+
shouldRunSettings: true,
|
|
76
|
+
shouldRunServer: false,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('resolveSettingsDevPort reserves APP_PORT + 3 by default', () => {
|
|
81
|
+
expect(resolveSettingsDevPort(13001)).toBe(13004);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('createSettingsDevProcessOptions uses the standalone config and settings HMR path', () => {
|
|
85
|
+
expect(
|
|
86
|
+
createSettingsDevProcessOptions({
|
|
87
|
+
appPackageRoot: '/repo/packages/core/app',
|
|
88
|
+
appPort: 13001,
|
|
89
|
+
settingsPort: 13004,
|
|
90
|
+
browserPort: 13001,
|
|
91
|
+
appPublicPath: '/nocobase/',
|
|
92
|
+
processEnv: { API_BASE_URL: '/api/' },
|
|
93
|
+
}),
|
|
94
|
+
).toMatchObject({
|
|
95
|
+
command: 'rsbuild',
|
|
96
|
+
args: ['dev', '--config', '/repo/packages/core/app/client-settings/rsbuild.config.ts'],
|
|
97
|
+
runOptions: {
|
|
98
|
+
prefix: 'client-settings',
|
|
99
|
+
env: {
|
|
100
|
+
APP_PORT: '13001',
|
|
101
|
+
APP_SETTINGS_PORT: '13004',
|
|
102
|
+
RSPACK_HMR_CLIENT_PORT: '13001',
|
|
103
|
+
RSPACK_HMR_PATH: '/nocobase/settings/__rspack_hmr',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
34
108
|
});
|
|
@@ -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,10 @@ const { resolve, posix } = require('path');
|
|
|
11
11
|
const { storagePathJoin, resolvePublicPath, resolveV2PublicPath, normalizeModernClientPrefix } = require('../util');
|
|
12
12
|
const { readFileSync, writeFileSync } = require('fs');
|
|
13
13
|
|
|
14
|
+
function escapeRegExp(value) {
|
|
15
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
19
|
*
|
|
16
20
|
* @param {Command} cli
|
|
@@ -24,6 +28,8 @@ module.exports = (cli) => {
|
|
|
24
28
|
const modernClientPrefix = normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX);
|
|
25
29
|
const appPublicPathWithoutTrailingSlash = appPublicPath.replace(/\/$/, '');
|
|
26
30
|
const v2PublicPathWithoutTrailingSlash = v2PublicPath.replace(/\/$/, '');
|
|
31
|
+
const settingsAssetsPath = `${appPublicPath}settings/assets/`;
|
|
32
|
+
const settingsDocumentPattern = `^${escapeRegExp(appPublicPath)}settings(?:/|$)`;
|
|
27
33
|
const file = resolve(__dirname, '../../nocobase.conf.tpl');
|
|
28
34
|
const data = readFileSync(file, 'utf-8');
|
|
29
35
|
let otherLocation = '';
|
|
@@ -38,6 +44,21 @@ module.exports = (cli) => {
|
|
|
38
44
|
return 302 ${appPublicPathWithoutTrailingSlash}$uri$is_args$args;
|
|
39
45
|
}
|
|
40
46
|
|
|
47
|
+
location ^~ /files/ {
|
|
48
|
+
proxy_pass http://127.0.0.1:${process.env.APP_PORT};
|
|
49
|
+
proxy_http_version 1.1;
|
|
50
|
+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
51
|
+
proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
|
|
52
|
+
proxy_set_header Host $final_host;
|
|
53
|
+
proxy_set_header Referer $http_referer;
|
|
54
|
+
proxy_set_header User-Agent $http_user_agent;
|
|
55
|
+
add_header Cache-Control 'no-cache, no-store';
|
|
56
|
+
proxy_connect_timeout 600;
|
|
57
|
+
proxy_send_timeout 600;
|
|
58
|
+
proxy_read_timeout 600;
|
|
59
|
+
send_timeout 600;
|
|
60
|
+
}
|
|
61
|
+
|
|
41
62
|
location / {
|
|
42
63
|
alias ${posix.resolve(process.cwd())}/node_modules/@nocobase/app/dist/client/;
|
|
43
64
|
try_files $uri $uri/ /index.html;
|
|
@@ -49,6 +70,8 @@ module.exports = (cli) => {
|
|
|
49
70
|
.replace(/\{\{distPath\}\}/g, distPath)
|
|
50
71
|
.replace(/\{\{v2PublicPath\}\}/g, v2PublicPath)
|
|
51
72
|
.replace(/\{\{v2PublicPathNoTrailingSlash\}\}/g, v2PublicPathWithoutTrailingSlash)
|
|
73
|
+
.replace(/\{\{settingsAssetsPath\}\}/g, settingsAssetsPath)
|
|
74
|
+
.replace(/\{\{settingsDocumentPattern\}\}/g, settingsDocumentPattern)
|
|
52
75
|
.replace(/\{\{apiPort\}\}/g, process.env.APP_PORT)
|
|
53
76
|
.replace(/\{\{otherLocation\}\}/g, otherLocation);
|
|
54
77
|
const targetFile = storagePathJoin('nocobase.conf');
|
package/src/commands/dev.js
CHANGED
|
@@ -20,6 +20,7 @@ const {
|
|
|
20
20
|
isPortReachable,
|
|
21
21
|
buildWSURL,
|
|
22
22
|
checkDBDialect,
|
|
23
|
+
resolveAppClientEntryMode,
|
|
23
24
|
} = require('../util');
|
|
24
25
|
const { getPortPromise } = require('portfinder');
|
|
25
26
|
const chokidar = require('chokidar');
|
|
@@ -47,6 +48,69 @@ function buildAppDevForwardArgs(argv = process.argv) {
|
|
|
47
48
|
return ['app-dev', ...argv.slice(3)];
|
|
48
49
|
}
|
|
49
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
|
+
|
|
93
|
+
function resolveDevRuntimeMode(opts = {}) {
|
|
94
|
+
const appClientEntryMode = opts.appClientEntryMode || resolveAppClientEntryMode();
|
|
95
|
+
const useModernOnlyEntryMode = appClientEntryMode === 'modern-only';
|
|
96
|
+
const clientV2Only = !!opts.clientV2Only;
|
|
97
|
+
const forceClient = !!opts.client;
|
|
98
|
+
const forceServer = !!opts.server;
|
|
99
|
+
const shouldRunClientV2 = clientV2Only || useModernOnlyEntryMode || forceClient || !forceServer;
|
|
100
|
+
const shouldRunClient = !clientV2Only && !useModernOnlyEntryMode && (forceClient || !forceServer);
|
|
101
|
+
const shouldRunServer = !clientV2Only && (forceServer || !forceClient || useModernOnlyEntryMode);
|
|
102
|
+
const shouldRunSettings = shouldRunClientV2;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
appClientEntryMode,
|
|
106
|
+
useModernOnlyEntryMode,
|
|
107
|
+
shouldRunClientV2,
|
|
108
|
+
shouldRunClient,
|
|
109
|
+
shouldRunServer,
|
|
110
|
+
shouldRunSettings,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
50
114
|
async function forwardDevToAppDev({ argv = process.argv, runCommand = run } = {}) {
|
|
51
115
|
await runCommand('nocobase-v1', buildAppDevForwardArgs(argv));
|
|
52
116
|
}
|
|
@@ -63,6 +127,7 @@ module.exports = (cli) => {
|
|
|
63
127
|
.option('-c, --client')
|
|
64
128
|
.option('-s, --server')
|
|
65
129
|
.option('--db-sync')
|
|
130
|
+
.option('--quickstart')
|
|
66
131
|
.option('--rsbuild')
|
|
67
132
|
.option('--client-v2-only')
|
|
68
133
|
.option('-i, --inspect [port]')
|
|
@@ -103,13 +168,18 @@ module.exports = (cli) => {
|
|
|
103
168
|
let clientPort = APP_PORT;
|
|
104
169
|
let serverPort;
|
|
105
170
|
let clientV2Port = APP_PORT;
|
|
171
|
+
let settingsPort = resolveSettingsDevPort(APP_PORT);
|
|
106
172
|
|
|
107
173
|
nodeCheck();
|
|
108
174
|
await postCheck(opts);
|
|
109
175
|
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
176
|
+
const {
|
|
177
|
+
useModernOnlyEntryMode,
|
|
178
|
+
shouldRunClientV2,
|
|
179
|
+
shouldRunClient,
|
|
180
|
+
shouldRunServer,
|
|
181
|
+
shouldRunSettings,
|
|
182
|
+
} = resolveDevRuntimeMode(opts);
|
|
113
183
|
const shouldRunClientWithRsbuild = shouldRunClient && !!rsbuild;
|
|
114
184
|
|
|
115
185
|
if (shouldRunServer && server) {
|
|
@@ -120,14 +190,25 @@ module.exports = (cli) => {
|
|
|
120
190
|
});
|
|
121
191
|
}
|
|
122
192
|
|
|
123
|
-
if (shouldRunClientV2 && !clientV2Only) {
|
|
193
|
+
if (shouldRunClientV2 && !clientV2Only && !useModernOnlyEntryMode) {
|
|
124
194
|
clientV2Port = await getPortPromise({
|
|
125
195
|
port: 1 * clientPort + 2,
|
|
126
196
|
});
|
|
127
197
|
}
|
|
128
198
|
|
|
199
|
+
if (useModernOnlyEntryMode) {
|
|
200
|
+
clientV2Port = APP_PORT;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (shouldRunSettings) {
|
|
204
|
+
settingsPort = await getPortPromise({
|
|
205
|
+
port: resolveSettingsDevPort(APP_PORT),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
129
209
|
let subprocessClient;
|
|
130
210
|
let subprocessClientV2;
|
|
211
|
+
let subprocessSettings;
|
|
131
212
|
|
|
132
213
|
const runDevClientV2 = () => {
|
|
133
214
|
console.log('starting client-v2', 1 * clientV2Port);
|
|
@@ -140,6 +221,7 @@ module.exports = (cli) => {
|
|
|
140
221
|
env: {
|
|
141
222
|
...process.env,
|
|
142
223
|
APP_V2_PORT: `${clientV2Port}`,
|
|
224
|
+
APP_SETTINGS_PORT: `${settingsPort}`,
|
|
143
225
|
NODE_ENV: 'development',
|
|
144
226
|
RSPACK_HMR_CLIENT_PORT: `${clientV2Only ? clientV2Port : clientPort}`,
|
|
145
227
|
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
|
@@ -157,8 +239,34 @@ module.exports = (cli) => {
|
|
|
157
239
|
);
|
|
158
240
|
};
|
|
159
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
|
+
|
|
160
267
|
if (clientV2Only) {
|
|
161
268
|
runDevClientV2();
|
|
269
|
+
runDevSettings();
|
|
162
270
|
return;
|
|
163
271
|
}
|
|
164
272
|
|
|
@@ -179,6 +287,7 @@ module.exports = (cli) => {
|
|
|
179
287
|
APP_PORT: `${clientPort}`,
|
|
180
288
|
APP_ROOT: `${APP_PACKAGE_ROOT}/client`,
|
|
181
289
|
APP_V2_PORT: `${clientV2Port}`,
|
|
290
|
+
APP_SETTINGS_PORT: `${settingsPort}`,
|
|
182
291
|
NODE_ENV: 'development',
|
|
183
292
|
RSPACK_HMR_CLIENT_PORT: `${clientPort}`,
|
|
184
293
|
API_BASE_URL: process.env.API_BASE_URL || process.env.API_BASE_PATH,
|
|
@@ -231,6 +340,9 @@ module.exports = (cli) => {
|
|
|
231
340
|
if (shouldRunClientV2) {
|
|
232
341
|
await restartSubprocess(subprocessClientV2, clientV2Port, runDevClientV2);
|
|
233
342
|
}
|
|
343
|
+
if (shouldRunSettings) {
|
|
344
|
+
await restartSubprocess(subprocessSettings, settingsPort, runDevSettings);
|
|
345
|
+
}
|
|
234
346
|
await fs.promises.writeFile(process.env.WATCH_FILE, `export const watchId = '${uid()}';`, 'utf-8');
|
|
235
347
|
}, 500);
|
|
236
348
|
|
|
@@ -300,10 +412,17 @@ module.exports = (cli) => {
|
|
|
300
412
|
if (shouldRunClientV2) {
|
|
301
413
|
runDevClientV2();
|
|
302
414
|
}
|
|
415
|
+
|
|
416
|
+
if (shouldRunSettings) {
|
|
417
|
+
runDevSettings();
|
|
418
|
+
}
|
|
303
419
|
});
|
|
304
420
|
};
|
|
305
421
|
|
|
306
422
|
module.exports._test = {
|
|
307
423
|
buildAppDevForwardArgs,
|
|
424
|
+
createSettingsDevProcessOptions,
|
|
308
425
|
forwardDevToAppDev,
|
|
426
|
+
resolveDevRuntimeMode,
|
|
427
|
+
resolveSettingsDevPort,
|
|
309
428
|
};
|
package/src/util.js
CHANGED
|
@@ -432,13 +432,41 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
|
432
432
|
|
|
433
433
|
exports.DEFAULT_MODERN_CLIENT_PREFIX = DEFAULT_MODERN_CLIENT_PREFIX;
|
|
434
434
|
|
|
435
|
+
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
|
436
|
+
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
|
437
|
+
|
|
438
|
+
exports.DEFAULT_APP_CLIENT_ENTRY_MODE = DEFAULT_APP_CLIENT_ENTRY_MODE;
|
|
439
|
+
|
|
440
|
+
function isAppClientEntryMode(value) {
|
|
441
|
+
return APP_CLIENT_ENTRY_MODES.has(String(value || '').trim());
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
exports.isAppClientEntryMode = isAppClientEntryMode;
|
|
445
|
+
|
|
446
|
+
function normalizeAppClientEntryMode(value) {
|
|
447
|
+
const normalized = String(value || '').trim();
|
|
448
|
+
return isAppClientEntryMode(normalized) ? normalized : DEFAULT_APP_CLIENT_ENTRY_MODE;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
exports.normalizeAppClientEntryMode = normalizeAppClientEntryMode;
|
|
452
|
+
|
|
453
|
+
function resolveAppClientEntryMode() {
|
|
454
|
+
return normalizeAppClientEntryMode(process.env.APP_CLIENT_ENTRY_MODE);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
exports.resolveAppClientEntryMode = resolveAppClientEntryMode;
|
|
458
|
+
|
|
435
459
|
// Normalize APP_MODERN_CLIENT_PREFIX (accepts `v`, `/v`, `/v/`)
|
|
436
460
|
// down to a bare segment like `v`.
|
|
437
461
|
function normalizeModernClientPrefix(value) {
|
|
438
462
|
const segment = String(value || '')
|
|
439
463
|
.trim()
|
|
440
464
|
.replace(/^\/+|\/+$/g, '');
|
|
441
|
-
|
|
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;
|
|
442
470
|
}
|
|
443
471
|
|
|
444
472
|
exports.normalizeModernClientPrefix = normalizeModernClientPrefix;
|
|
@@ -455,6 +483,13 @@ function isAppDevHtml() {
|
|
|
455
483
|
return process.argv[2] === 'app-dev' || process.env.NOCOBASE_APP_DEV === 'true';
|
|
456
484
|
}
|
|
457
485
|
|
|
486
|
+
function shouldRefreshLegacyIndexTemplate(data = '') {
|
|
487
|
+
return (
|
|
488
|
+
!data.includes("window['__nocobase_modern_client_prefix__']") ||
|
|
489
|
+
!data.includes("window['__nocobase_app_client_entry_mode__']")
|
|
490
|
+
);
|
|
491
|
+
}
|
|
492
|
+
|
|
458
493
|
function buildIndexHtml(force = false) {
|
|
459
494
|
if (process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS === 'true') {
|
|
460
495
|
return;
|
|
@@ -469,11 +504,15 @@ function buildIndexHtml(force = false) {
|
|
|
469
504
|
}
|
|
470
505
|
if (!fs.existsSync(tpl)) {
|
|
471
506
|
fs.copyFileSync(file, tpl);
|
|
507
|
+
} else if (shouldRefreshLegacyIndexTemplate(fs.readFileSync(tpl, 'utf-8'))) {
|
|
508
|
+
fs.copyFileSync(file, tpl);
|
|
472
509
|
}
|
|
473
510
|
const data = fs.readFileSync(tpl, 'utf-8');
|
|
474
511
|
let replacedData = data
|
|
475
512
|
.replace(/\{\{env.CDN_BASE_URL\}\}/g, process.env.CDN_BASE_URL)
|
|
476
513
|
.replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
|
|
514
|
+
.replace(/\{\{env.APP_MODERN_CLIENT_PREFIX\}\}/g, normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX))
|
|
515
|
+
.replace(/\{\{env.APP_CLIENT_ENTRY_MODE\}\}/g, resolveAppClientEntryMode())
|
|
477
516
|
.replace(/\{\{env.API_CLIENT_SHARE_TOKEN\}\}/g, process.env.API_CLIENT_SHARE_TOKEN || 'false')
|
|
478
517
|
.replace(/\{\{env.API_CLIENT_STORAGE_TYPE\}\}/g, process.env.API_CLIENT_STORAGE_TYPE)
|
|
479
518
|
.replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX)
|
|
@@ -614,6 +653,7 @@ exports.initEnv = function initEnv() {
|
|
|
614
653
|
CDN_BASE_URL: '',
|
|
615
654
|
APP_PUBLIC_PATH: '/',
|
|
616
655
|
APP_MODERN_CLIENT_PREFIX: DEFAULT_MODERN_CLIENT_PREFIX,
|
|
656
|
+
APP_CLIENT_ENTRY_MODE: DEFAULT_APP_CLIENT_ENTRY_MODE,
|
|
617
657
|
ESM_CDN_BASE_URL: 'https://esm.sh',
|
|
618
658
|
ESM_CDN_SUFFIX: '',
|
|
619
659
|
};
|
|
@@ -665,6 +705,14 @@ exports.initEnv = function initEnv() {
|
|
|
665
705
|
}
|
|
666
706
|
}
|
|
667
707
|
|
|
708
|
+
const rawAppClientEntryMode = String(process.env.APP_CLIENT_ENTRY_MODE || '').trim();
|
|
709
|
+
if (rawAppClientEntryMode && !isAppClientEntryMode(rawAppClientEntryMode)) {
|
|
710
|
+
console.warn(
|
|
711
|
+
`Unknown APP_CLIENT_ENTRY_MODE "${rawAppClientEntryMode}", falling back to ${DEFAULT_APP_CLIENT_ENTRY_MODE}.`,
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
process.env.APP_CLIENT_ENTRY_MODE = normalizeAppClientEntryMode(rawAppClientEntryMode);
|
|
715
|
+
|
|
668
716
|
if (!process.env.__env_modified__ && process.env.APP_PUBLIC_PATH) {
|
|
669
717
|
const publicPath = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '');
|
|
670
718
|
const keys = ['API_BASE_PATH', 'WS_PATH', 'PLUGIN_STATICS_PATH'];
|