@nocobase/cli-v1 2.2.0-alpha.1 → 2.2.0-alpha.11

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 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
 
@@ -163,7 +167,72 @@ server {
163
167
  send_timeout 600;
164
168
  }
165
169
 
170
+ # File access URLs are application routes. Keep them above the SPA
171
+ # locations so Nginx forwards them to the NocoBase gateway.
172
+ location ^~ {{publicPath}}files/ {
173
+ proxy_pass http://127.0.0.1:{{apiPort}};
174
+ proxy_http_version 1.1;
175
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
176
+ proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
177
+ proxy_set_header Host $final_host;
178
+ proxy_set_header Referer $http_referer;
179
+ proxy_set_header User-Agent $http_user_agent;
180
+ add_header Cache-Control 'no-cache, no-store';
181
+ proxy_connect_timeout 600;
182
+ proxy_send_timeout 600;
183
+ proxy_read_timeout 600;
184
+ send_timeout 600;
185
+ }
186
+
187
+
188
+
189
+ location ^~ {{publicPath}}x/apps/ {
190
+ absolute_redirect off;
191
+
192
+ if ($uri ~ ^{{publicPath}}x/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {
193
+ return 308 {{publicPath}}x/apps/$subapp/$portal/$is_args$args;
194
+ }
195
+
196
+ if ($uri !~ ^{{publicPath}}x/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
197
+ return 404;
198
+ }
199
+
200
+ root {{cwd}}/storage;
201
+
202
+ if ($portal_path = "") {
203
+ rewrite ^ /portals/$subapp/$portal/dist/index.html break;
204
+ }
205
+
206
+ try_files
207
+ /portals/$subapp/$portal/dist/$portal_path
208
+ /portals/$subapp/$portal/dist/$portal_path/
209
+ /portals/$subapp/$portal/dist/index.html
210
+ =404;
211
+ }
212
+
213
+ location ^~ {{publicPath}}x/ {
214
+ absolute_redirect off;
166
215
 
216
+ if ($uri ~ ^{{publicPath}}x/(?<portal>[A-Za-z0-9_-]+)$) {
217
+ return 308 {{publicPath}}x/$portal/$is_args$args;
218
+ }
219
+
220
+ if ($uri !~ ^{{publicPath}}x/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {
221
+ return 404;
222
+ }
223
+
224
+ root {{cwd}}/storage;
225
+
226
+ if ($portal_path = "") {
227
+ rewrite ^ /portals/main/$portal/dist/index.html break;
228
+ }
229
+
230
+ try_files
231
+ /portals/main/$portal/dist/$portal_path
232
+ /portals/main/$portal/dist/$portal_path/
233
+ /portals/main/$portal/dist/index.html
234
+ =404;
235
+ }
167
236
 
168
237
  location {{publicPath}} {
169
238
  alias {{cwd}}/node_modules/@nocobase/app/dist/client/;
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "@nocobase/cli-v1",
3
- "version": "2.2.0-alpha.1",
3
+ "version": "2.2.0-alpha.11",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./src/index.js",
7
7
  "bin": {
8
+ "nocobase": "./bin/index.js",
8
9
  "nocobase-v1": "./bin/index.js"
9
10
  },
10
11
  "dependencies": {
11
- "@nocobase/cli": "2.2.0-alpha.1",
12
+ "@nocobase/cli": "2.2.0-alpha.11",
12
13
  "@nocobase/license-kit": "^0.3.8",
13
- "@nocobase/utils": "2.2.0-alpha.1",
14
+ "@nocobase/utils": "2.2.0-alpha.11",
14
15
  "chalk": "^4.1.1",
15
16
  "commander": "^9.2.0",
16
17
  "deepmerge": "^4.3.1",
@@ -25,7 +26,6 @@
25
26
  "tree-kill": "^1.2.2"
26
27
  },
27
28
  "devDependencies": {
28
- "@nocobase/devtools": "2.2.0-alpha.1",
29
29
  "@types/fs-extra": "^11.0.1"
30
30
  },
31
31
  "repository": {
@@ -33,5 +33,5 @@
33
33
  "url": "git+https://github.com/nocobase/nocobase.git",
34
34
  "directory": "packages/core/cli"
35
35
  },
36
- "gitHead": "303663aba6c6eefa27e6a6435b4c0352074ec40f"
36
+ "gitHead": "c9ff1f51e5c33bc6437b64eb779212be71b78f58"
37
37
  }
@@ -82,6 +82,7 @@ describe('cli-v1 app-dev utils', () => {
82
82
  }),
83
83
  ).toEqual([
84
84
  'watch',
85
+ '--clear-screen=false',
85
86
  `--ignore=${path.resolve(process.cwd(), 'storage/plugins')}/**`,
86
87
  '--tsconfig',
87
88
  './tsconfig.server.json',
@@ -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,70 @@
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 { buildAppDevForwardArgs, forwardDevToAppDev, resolveDevRuntimeMode } = require('../commands/dev')._test;
13
+
14
+ describe('cli-v1 dev command', () => {
15
+ test('buildAppDevForwardArgs rewrites dev argv to app-dev while preserving extra args', () => {
16
+ expect(
17
+ buildAppDevForwardArgs([
18
+ 'node',
19
+ 'nocobase-v1',
20
+ 'dev',
21
+ '--rsbuild',
22
+ '--quickstart',
23
+ '--port',
24
+ '13000',
25
+ '--inspect=9229',
26
+ ]),
27
+ ).toEqual(['app-dev', '--rsbuild', '--quickstart', '--port', '13000', '--inspect=9229']);
28
+ });
29
+
30
+ test('forwardDevToAppDev delegates to nocobase-v1 app-dev for create-app projects', async () => {
31
+ const calls = [];
32
+ const runCommand = async (...args) => {
33
+ calls.push(args);
34
+ };
35
+
36
+ await forwardDevToAppDev({
37
+ argv: ['node', 'nocobase-v1', 'dev', '--port', '13000', '--db-sync'],
38
+ runCommand,
39
+ });
40
+
41
+ expect(calls).toEqual([['nocobase-v1', ['app-dev', '--port', '13000', '--db-sync']]]);
42
+ });
43
+
44
+ test('resolveDevRuntimeMode keeps both clients in legacy-default', () => {
45
+ expect(resolveDevRuntimeMode({ appClientEntryMode: 'legacy-default' })).toMatchObject({
46
+ useModernOnlyEntryMode: false,
47
+ shouldRunClient: true,
48
+ shouldRunClientV2: true,
49
+ shouldRunServer: true,
50
+ });
51
+ });
52
+
53
+ test('resolveDevRuntimeMode only runs v2 client and server in modern-only', () => {
54
+ expect(resolveDevRuntimeMode({ appClientEntryMode: 'modern-only' })).toMatchObject({
55
+ useModernOnlyEntryMode: true,
56
+ shouldRunClient: false,
57
+ shouldRunClientV2: true,
58
+ shouldRunServer: true,
59
+ });
60
+ });
61
+
62
+ test('resolveDevRuntimeMode preserves explicit client-v2-only flag behavior', () => {
63
+ expect(resolveDevRuntimeMode({ clientV2Only: true, appClientEntryMode: 'legacy-default' })).toMatchObject({
64
+ useModernOnlyEntryMode: false,
65
+ shouldRunClient: false,
66
+ shouldRunClientV2: true,
67
+ shouldRunServer: false,
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,33 @@
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 { colorizedDevLogEnv, createRunWithPrefixLabel } = require('../util')._test;
13
+
14
+ describe('cli-v1 util helpers', () => {
15
+ test('colorizedDevLogEnv enables color for dev child output by default', () => {
16
+ expect(colorizedDevLogEnv({})).toEqual({ FORCE_COLOR: '1' });
17
+ });
18
+
19
+ test('colorizedDevLogEnv keeps explicit FORCE_COLOR', () => {
20
+ expect(colorizedDevLogEnv({ FORCE_COLOR: '3' })).toEqual({ FORCE_COLOR: '3' });
21
+ });
22
+
23
+ test('colorizedDevLogEnv removes inherited NO_COLOR so dev logs stay colored', () => {
24
+ expect(colorizedDevLogEnv({ NO_COLOR: '1', TERM: 'dumb' })).toEqual({
25
+ FORCE_COLOR: '1',
26
+ TERM: 'dumb',
27
+ });
28
+ });
29
+
30
+ test('createRunWithPrefixLabel colorizes prefixed output even when the parent disables colors', () => {
31
+ expect(createRunWithPrefixLabel('client', 'cyan')).toBe('\u001b[36m[client]\u001b[39m');
32
+ });
33
+ });
@@ -150,7 +150,7 @@ function buildAppDevServerArgs({
150
150
  argv = process.argv,
151
151
  serverTsconfigPath = process.env.SERVER_TSCONFIG_PATH,
152
152
  } = {}) {
153
- const args = ['watch', `--ignore=${resolvePluginStoragePath()}/**`];
153
+ const args = ['watch', '--clear-screen=false', `--ignore=${resolvePluginStoragePath()}/**`];
154
154
 
155
155
  if (serverTsconfigPath) {
156
156
  args.push('--tsconfig', serverTsconfigPath);
@@ -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 { storagePathJoin } = require('../util');
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;
@@ -38,6 +38,21 @@ module.exports = (cli) => {
38
38
  return 302 ${appPublicPathWithoutTrailingSlash}$uri$is_args$args;
39
39
  }
40
40
 
41
+ location ^~ /files/ {
42
+ proxy_pass http://127.0.0.1:${process.env.APP_PORT};
43
+ proxy_http_version 1.1;
44
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
45
+ proxy_set_header X-Forwarded-Proto $upstream_x_forwarded_proto;
46
+ proxy_set_header Host $final_host;
47
+ proxy_set_header Referer $http_referer;
48
+ proxy_set_header User-Agent $http_user_agent;
49
+ add_header Cache-Control 'no-cache, no-store';
50
+ proxy_connect_timeout 600;
51
+ proxy_send_timeout 600;
52
+ proxy_read_timeout 600;
53
+ send_timeout 600;
54
+ }
55
+
41
56
  location / {
42
57
  alias ${posix.resolve(process.cwd())}/node_modules/@nocobase/app/dist/client/;
43
58
  try_files $uri $uri/ /index.html;
@@ -10,14 +10,17 @@ const _ = require('lodash');
10
10
  const { Command } = require('commander');
11
11
  const {
12
12
  generatePlugins,
13
+ hasCorePackages,
13
14
  run,
14
15
  runWithPrefix,
16
+ colorizedDevLogEnv,
15
17
  postCheck,
16
18
  nodeCheck,
17
19
  promptForTs,
18
20
  isPortReachable,
19
21
  buildWSURL,
20
22
  checkDBDialect,
23
+ resolveAppClientEntryMode,
21
24
  } = require('../util');
22
25
  const { getPortPromise } = require('portfinder');
23
26
  const chokidar = require('chokidar');
@@ -41,6 +44,33 @@ async function buildBundleStatusHtml() {
41
44
  );
42
45
  }
43
46
 
47
+ function buildAppDevForwardArgs(argv = process.argv) {
48
+ return ['app-dev', ...argv.slice(3)];
49
+ }
50
+
51
+ function resolveDevRuntimeMode(opts = {}) {
52
+ const appClientEntryMode = opts.appClientEntryMode || resolveAppClientEntryMode();
53
+ const useModernOnlyEntryMode = appClientEntryMode === 'modern-only';
54
+ const clientV2Only = !!opts.clientV2Only;
55
+ const forceClient = !!opts.client;
56
+ const forceServer = !!opts.server;
57
+ const shouldRunClientV2 = clientV2Only || useModernOnlyEntryMode || forceClient || !forceServer;
58
+ const shouldRunClient = !clientV2Only && !useModernOnlyEntryMode && (forceClient || !forceServer);
59
+ const shouldRunServer = !clientV2Only && (forceServer || !forceClient || useModernOnlyEntryMode);
60
+
61
+ return {
62
+ appClientEntryMode,
63
+ useModernOnlyEntryMode,
64
+ shouldRunClientV2,
65
+ shouldRunClient,
66
+ shouldRunServer,
67
+ };
68
+ }
69
+
70
+ async function forwardDevToAppDev({ argv = process.argv, runCommand = run } = {}) {
71
+ await runCommand('nocobase-v1', buildAppDevForwardArgs(argv));
72
+ }
73
+
44
74
  /**
45
75
  *
46
76
  * @param {Command} cli
@@ -53,11 +83,17 @@ module.exports = (cli) => {
53
83
  .option('-c, --client')
54
84
  .option('-s, --server')
55
85
  .option('--db-sync')
86
+ .option('--quickstart')
56
87
  .option('--rsbuild')
57
88
  .option('--client-v2-only')
58
89
  .option('-i, --inspect [port]')
59
90
  .allowUnknownOption()
60
91
  .action(async (opts) => {
92
+ if (!hasCorePackages()) {
93
+ await forwardDevToAppDev();
94
+ return;
95
+ }
96
+
61
97
  checkDBDialect();
62
98
  await buildBundleStatusHtml();
63
99
 
@@ -92,9 +128,9 @@ module.exports = (cli) => {
92
128
  nodeCheck();
93
129
  await postCheck(opts);
94
130
 
95
- const shouldRunClientV2 = clientV2Only || client || !server;
96
- const shouldRunClient = !clientV2Only && (client || !server);
97
- const shouldRunServer = !clientV2Only && (server || !client);
131
+ const { useModernOnlyEntryMode, shouldRunClientV2, shouldRunClient, shouldRunServer } = resolveDevRuntimeMode(
132
+ opts,
133
+ );
98
134
  const shouldRunClientWithRsbuild = shouldRunClient && !!rsbuild;
99
135
 
100
136
  if (shouldRunServer && server) {
@@ -105,12 +141,16 @@ module.exports = (cli) => {
105
141
  });
106
142
  }
107
143
 
108
- if (shouldRunClientV2 && !clientV2Only) {
144
+ if (shouldRunClientV2 && !clientV2Only && !useModernOnlyEntryMode) {
109
145
  clientV2Port = await getPortPromise({
110
146
  port: 1 * clientPort + 2,
111
147
  });
112
148
  }
113
149
 
150
+ if (useModernOnlyEntryMode) {
151
+ clientV2Port = APP_PORT;
152
+ }
153
+
114
154
  let subprocessClient;
115
155
  let subprocessClientV2;
116
156
 
@@ -184,16 +224,13 @@ module.exports = (cli) => {
184
224
  }
185
225
  subprocessRef.cancel();
186
226
  let i = 0;
187
- while (true) {
227
+ while (i <= 10) {
188
228
  ++i;
189
229
  const result = await isPortReachable(port);
190
230
  if (!result) {
191
231
  break;
192
232
  }
193
233
  await sleep(500);
194
- if (i > 10) {
195
- break;
196
- }
197
234
  }
198
235
  start();
199
236
  };
@@ -202,7 +239,7 @@ module.exports = (cli) => {
202
239
  const storagePluginPath = resolvePluginStoragePath();
203
240
  const watcher = chokidar.watch(`${storagePluginPath}/**/*`, {
204
241
  cwd: process.cwd(),
205
- ignored: /(^|[\/\\])\../, // 忽略隐藏文件
242
+ ignored: /(^|[/\\])\../, // 忽略隐藏文件
206
243
  persistent: true,
207
244
  depth: 1, // 只监听第一层目录
208
245
  });
@@ -246,6 +283,7 @@ module.exports = (cli) => {
246
283
 
247
284
  const argv = [
248
285
  'watch',
286
+ '--clear-screen=false',
249
287
  ...(inspect ? [`--inspect=${inspect === true ? 9229 : inspect}`] : []),
250
288
  `--ignore=${resolvePluginStoragePath()}/**`,
251
289
  '--tsconfig',
@@ -264,9 +302,9 @@ module.exports = (cli) => {
264
302
 
265
303
  const runDevServer = () => {
266
304
  run('tsx', argv, {
267
- env: {
305
+ env: colorizedDevLogEnv(process.env, {
268
306
  APP_PORT: serverPort,
269
- },
307
+ }),
270
308
  }).catch((err) => {
271
309
  if (err.exitCode == 100) {
272
310
  console.log('Restarting server...');
@@ -289,3 +327,9 @@ module.exports = (cli) => {
289
327
  }
290
328
  });
291
329
  };
330
+
331
+ module.exports._test = {
332
+ buildAppDevForwardArgs,
333
+ forwardDevToAppDev,
334
+ resolveDevRuntimeMode,
335
+ };
@@ -68,6 +68,14 @@ module.exports = (cli) => {
68
68
  if (descJson['dependencies']?.['@nocobase/app']) {
69
69
  descJson['dependencies']['@nocobase/app'] = stdout;
70
70
  }
71
+ descJson['scripts']['dev'] = 'nocobase app-dev';
72
+ if (descJson['dependencies']?.['@nocobase/cli']) {
73
+ descJson['dependencies']['@nocobase/app'] = stdout;
74
+ console.log(
75
+ chalk.yellow('The @nocobase/cli package is no longer needed, it will be removed from dependencies.'),
76
+ );
77
+ delete descJson['dependencies']['@nocobase/cli'];
78
+ }
71
79
  if (descJson['devDependencies']?.['@nocobase/devtools']) {
72
80
  descJson['devDependencies']['@nocobase/devtools'] = stdout;
73
81
  }
package/src/util.js CHANGED
@@ -84,6 +84,24 @@ exports.run = (command, args, options = {}) => {
84
84
  });
85
85
  };
86
86
 
87
+ function colorizedDevLogEnv(env = process.env, overrides = {}) {
88
+ const nextEnv = {
89
+ ...env,
90
+ ...overrides,
91
+ };
92
+ delete nextEnv.NO_COLOR;
93
+ if (!nextEnv.FORCE_COLOR) {
94
+ nextEnv.FORCE_COLOR = '1';
95
+ }
96
+ return nextEnv;
97
+ }
98
+
99
+ function createRunWithPrefixLabel(prefix, color) {
100
+ const forcedChalk = new chalk.Instance({ level: 1 });
101
+ const colorize = forcedChalk[color] || forcedChalk.cyan;
102
+ return colorize(`[${prefix}]`);
103
+ }
104
+
87
105
  exports.runWithPrefix = (command, args, options = {}) => {
88
106
  if (command === 'tsx') {
89
107
  command = 'node';
@@ -92,15 +110,12 @@ exports.runWithPrefix = (command, args, options = {}) => {
92
110
 
93
111
  const prefix = options.prefix || 'process';
94
112
  const color = options.color || 'cyan';
95
- const label = chalk[color](`[${prefix}]`);
113
+ const label = createRunWithPrefixLabel(prefix, color);
96
114
  const subprocess = execa(command, args, {
97
115
  shell: true,
98
116
  stdio: 'pipe',
99
117
  ...options,
100
- env: {
101
- ...process.env,
102
- ...options.env,
103
- },
118
+ env: colorizedDevLogEnv(process.env, options.env),
104
119
  });
105
120
 
106
121
  const writePrefixed = (chunk, writer) => {
@@ -122,6 +137,13 @@ exports.runWithPrefix = (command, args, options = {}) => {
122
137
  return subprocess;
123
138
  };
124
139
 
140
+ exports._test = {
141
+ createRunWithPrefixLabel,
142
+ colorizedDevLogEnv,
143
+ };
144
+
145
+ exports.colorizedDevLogEnv = colorizedDevLogEnv;
146
+
125
147
  exports.isPortReachable = async (port, { timeout = 1000, host } = {}) => {
126
148
  const promise = new Promise((resolve, reject) => {
127
149
  const socket = new net.Socket();
@@ -410,6 +432,30 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
410
432
 
411
433
  exports.DEFAULT_MODERN_CLIENT_PREFIX = DEFAULT_MODERN_CLIENT_PREFIX;
412
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
+
413
459
  // Normalize APP_MODERN_CLIENT_PREFIX (accepts `v`, `/v`, `/v/`)
414
460
  // down to a bare segment like `v`.
415
461
  function normalizeModernClientPrefix(value) {
@@ -433,7 +479,17 @@ function isAppDevHtml() {
433
479
  return process.argv[2] === 'app-dev' || process.env.NOCOBASE_APP_DEV === 'true';
434
480
  }
435
481
 
482
+ function shouldRefreshLegacyIndexTemplate(data = '') {
483
+ return (
484
+ !data.includes("window['__nocobase_modern_client_prefix__']") ||
485
+ !data.includes("window['__nocobase_app_client_entry_mode__']")
486
+ );
487
+ }
488
+
436
489
  function buildIndexHtml(force = false) {
490
+ if (process.env.NOCOBASE_EXTRACT_CLIENT_ASSETS === 'true') {
491
+ return;
492
+ }
437
493
  const file = `${process.env.APP_PACKAGE_ROOT}/dist/client/index.html`;
438
494
  if (!fs.existsSync(file)) {
439
495
  return;
@@ -444,11 +500,15 @@ function buildIndexHtml(force = false) {
444
500
  }
445
501
  if (!fs.existsSync(tpl)) {
446
502
  fs.copyFileSync(file, tpl);
503
+ } else if (shouldRefreshLegacyIndexTemplate(fs.readFileSync(tpl, 'utf-8'))) {
504
+ fs.copyFileSync(file, tpl);
447
505
  }
448
506
  const data = fs.readFileSync(tpl, 'utf-8');
449
507
  let replacedData = data
450
508
  .replace(/\{\{env.CDN_BASE_URL\}\}/g, process.env.CDN_BASE_URL)
451
509
  .replace(/\{\{env.APP_PUBLIC_PATH\}\}/g, process.env.APP_PUBLIC_PATH)
510
+ .replace(/\{\{env.APP_MODERN_CLIENT_PREFIX\}\}/g, normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX))
511
+ .replace(/\{\{env.APP_CLIENT_ENTRY_MODE\}\}/g, resolveAppClientEntryMode())
452
512
  .replace(/\{\{env.API_CLIENT_SHARE_TOKEN\}\}/g, process.env.API_CLIENT_SHARE_TOKEN || 'false')
453
513
  .replace(/\{\{env.API_CLIENT_STORAGE_TYPE\}\}/g, process.env.API_CLIENT_STORAGE_TYPE)
454
514
  .replace(/\{\{env.API_CLIENT_STORAGE_PREFIX\}\}/g, process.env.API_CLIENT_STORAGE_PREFIX)
@@ -589,6 +649,7 @@ exports.initEnv = function initEnv() {
589
649
  CDN_BASE_URL: '',
590
650
  APP_PUBLIC_PATH: '/',
591
651
  APP_MODERN_CLIENT_PREFIX: DEFAULT_MODERN_CLIENT_PREFIX,
652
+ APP_CLIENT_ENTRY_MODE: DEFAULT_APP_CLIENT_ENTRY_MODE,
592
653
  ESM_CDN_BASE_URL: 'https://esm.sh',
593
654
  ESM_CDN_SUFFIX: '',
594
655
  };
@@ -640,6 +701,14 @@ exports.initEnv = function initEnv() {
640
701
  }
641
702
  }
642
703
 
704
+ const rawAppClientEntryMode = String(process.env.APP_CLIENT_ENTRY_MODE || '').trim();
705
+ if (rawAppClientEntryMode && !isAppClientEntryMode(rawAppClientEntryMode)) {
706
+ console.warn(
707
+ `Unknown APP_CLIENT_ENTRY_MODE "${rawAppClientEntryMode}", falling back to ${DEFAULT_APP_CLIENT_ENTRY_MODE}.`,
708
+ );
709
+ }
710
+ process.env.APP_CLIENT_ENTRY_MODE = normalizeAppClientEntryMode(rawAppClientEntryMode);
711
+
643
712
  if (!process.env.__env_modified__ && process.env.APP_PUBLIC_PATH) {
644
713
  const publicPath = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '');
645
714
  const keys = ['API_BASE_PATH', 'WS_PATH', 'PLUGIN_STATICS_PATH'];