@nocobase/cli-v1 2.3.0-alpha.1 → 3.0.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 +81 -1
- package/package.json +4 -4
- 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 +50 -3
- package/src/__tests__/util.test.js +5 -0
- package/src/commands/client.js +98 -2
- package/src/commands/create-nginx-conf.js +8 -0
- package/src/commands/dev.js +97 -3
- package/src/util.js +5 -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;
|
|
@@ -182,6 +214,54 @@ server {
|
|
|
182
214
|
|
|
183
215
|
|
|
184
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
|
+
}
|
|
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
|
+
}
|
|
264
|
+
|
|
185
265
|
location {{publicPath}} {
|
|
186
266
|
alias {{cwd}}/node_modules/@nocobase/app/dist/client/;
|
|
187
267
|
try_files $uri $uri/ /index.html;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/cli-v1",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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": "
|
|
12
|
+
"@nocobase/cli": "3.0.0-alpha.2",
|
|
13
13
|
"@nocobase/license-kit": "^0.3.8",
|
|
14
|
-
"@nocobase/utils": "
|
|
14
|
+
"@nocobase/utils": "3.0.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": "a3faea21082b30989301de04591ee1b91fd4e6a5"
|
|
37
37
|
}
|
|
@@ -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 () => {
|
|
@@ -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,7 @@ describe('cli-v1 dev command', () => {
|
|
|
46
62
|
useModernOnlyEntryMode: true,
|
|
47
63
|
shouldRunClient: false,
|
|
48
64
|
shouldRunClientV2: true,
|
|
65
|
+
shouldRunSettings: true,
|
|
49
66
|
shouldRunServer: true,
|
|
50
67
|
});
|
|
51
68
|
});
|
|
@@ -55,7 +72,37 @@ describe('cli-v1 dev command', () => {
|
|
|
55
72
|
useModernOnlyEntryMode: false,
|
|
56
73
|
shouldRunClient: false,
|
|
57
74
|
shouldRunClientV2: true,
|
|
75
|
+
shouldRunSettings: true,
|
|
58
76
|
shouldRunServer: false,
|
|
59
77
|
});
|
|
60
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
|
+
});
|
|
61
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 = '';
|
|
@@ -64,6 +70,8 @@ module.exports = (cli) => {
|
|
|
64
70
|
.replace(/\{\{distPath\}\}/g, distPath)
|
|
65
71
|
.replace(/\{\{v2PublicPath\}\}/g, v2PublicPath)
|
|
66
72
|
.replace(/\{\{v2PublicPathNoTrailingSlash\}\}/g, v2PublicPathWithoutTrailingSlash)
|
|
73
|
+
.replace(/\{\{settingsAssetsPath\}\}/g, settingsAssetsPath)
|
|
74
|
+
.replace(/\{\{settingsDocumentPattern\}\}/g, settingsDocumentPattern)
|
|
67
75
|
.replace(/\{\{apiPort\}\}/g, process.env.APP_PORT)
|
|
68
76
|
.replace(/\{\{otherLocation\}\}/g, otherLocation);
|
|
69
77
|
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/util.js
CHANGED
|
@@ -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;
|