@nocobase/cli 2.1.31 → 2.2.0-alpha.10
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/dist/commands/config/set.js +1 -0
- package/dist/commands/init.js +119 -3
- package/dist/commands/install.js +97 -2
- package/dist/commands/portal/config.js +88 -0
- package/dist/commands/portal/create.js +104 -0
- package/dist/commands/portal/deploy.js +81 -0
- package/dist/commands/portal/destroy.js +104 -0
- package/dist/commands/portal/dev.js +71 -0
- package/dist/commands/portal/index.js +20 -0
- package/dist/commands/portal/info.js +82 -0
- package/dist/commands/portal/list.js +98 -0
- package/dist/commands/portal/pull.js +84 -0
- package/dist/commands/portal/push.js +79 -0
- package/dist/commands/source/dev.js +1 -1
- package/dist/lib/auth-store.js +3 -1
- package/dist/lib/cli-config.js +20 -1
- package/dist/lib/env-config.js +3 -0
- package/dist/lib/env-proxy.js +94 -2
- package/dist/lib/managed-init-env.js +6 -1
- package/dist/lib/portal-command-env.js +31 -0
- package/dist/lib/portal-config.js +133 -0
- package/dist/lib/portal-configure.js +117 -0
- package/dist/lib/portal-create.js +433 -0
- package/dist/lib/portal-deploy.js +283 -0
- package/dist/lib/portal-destroy.js +100 -0
- package/dist/lib/portal-dev.js +79 -0
- package/dist/lib/portal-env-files.js +53 -0
- package/dist/lib/portal-info.js +31 -0
- package/dist/lib/portal-list.js +211 -0
- package/dist/lib/portal-source.js +523 -0
- package/dist/lib/run-npm.js +17 -16
- package/dist/lib/ui.js +28 -1
- package/dist/locale/en-US.json +178 -0
- package/dist/locale/zh-CN.json +178 -0
- package/package.json +5 -2
package/dist/lib/cli-config.js
CHANGED
|
@@ -34,6 +34,7 @@ export const SUPPORTED_CLI_CONFIG_KEYS = [
|
|
|
34
34
|
'locale',
|
|
35
35
|
'default-ui-host',
|
|
36
36
|
'default-api-host',
|
|
37
|
+
'default-portal-template',
|
|
37
38
|
'update.policy',
|
|
38
39
|
'license.pkg-url',
|
|
39
40
|
'docker.network',
|
|
@@ -120,7 +121,10 @@ function pruneSettings(config) {
|
|
|
120
121
|
delete config.settings.locale;
|
|
121
122
|
}
|
|
122
123
|
const init = config.settings?.init;
|
|
123
|
-
if (init &&
|
|
124
|
+
if (init &&
|
|
125
|
+
!trimValue(init.defaultUiHost) &&
|
|
126
|
+
!trimValue(init.defaultApiHost) &&
|
|
127
|
+
!trimValue(init.defaultPortalTemplate)) {
|
|
124
128
|
delete config.settings?.init;
|
|
125
129
|
}
|
|
126
130
|
const update = config.settings?.update;
|
|
@@ -181,6 +185,8 @@ export function getExplicitCliConfigValue(config, key) {
|
|
|
181
185
|
return trimValue(config.settings?.init?.defaultUiHost);
|
|
182
186
|
case 'default-api-host':
|
|
183
187
|
return trimValue(config.settings?.init?.defaultApiHost);
|
|
188
|
+
case 'default-portal-template':
|
|
189
|
+
return trimValue(config.settings?.init?.defaultPortalTemplate);
|
|
184
190
|
case 'update.policy':
|
|
185
191
|
return normalizeCliUpdatePolicy(config.settings?.update?.policy);
|
|
186
192
|
case 'license.pkg-url':
|
|
@@ -233,6 +239,8 @@ export function getEffectiveCliConfigValue(config, key) {
|
|
|
233
239
|
return '127.0.0.1';
|
|
234
240
|
case 'default-api-host':
|
|
235
241
|
return '127.0.0.1';
|
|
242
|
+
case 'default-portal-template':
|
|
243
|
+
return explicit ?? '';
|
|
236
244
|
case 'update.policy':
|
|
237
245
|
return explicit ?? DEFAULT_UPDATE_POLICY;
|
|
238
246
|
case 'license.pkg-url':
|
|
@@ -377,6 +385,12 @@ export async function setCliConfigValue(key, value, options = {}) {
|
|
|
377
385
|
defaultApiHost: normalized,
|
|
378
386
|
};
|
|
379
387
|
break;
|
|
388
|
+
case 'default-portal-template':
|
|
389
|
+
config.settings.init = {
|
|
390
|
+
...(config.settings.init ?? {}),
|
|
391
|
+
defaultPortalTemplate: normalized,
|
|
392
|
+
};
|
|
393
|
+
break;
|
|
380
394
|
case 'update.policy':
|
|
381
395
|
config.settings.update = {
|
|
382
396
|
...(config.settings.update ?? {}),
|
|
@@ -512,6 +526,11 @@ export async function deleteCliConfigValue(key, options = {}) {
|
|
|
512
526
|
delete config.settings.init.defaultApiHost;
|
|
513
527
|
}
|
|
514
528
|
break;
|
|
529
|
+
case 'default-portal-template':
|
|
530
|
+
if (config.settings.init) {
|
|
531
|
+
delete config.settings.init.defaultPortalTemplate;
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
515
534
|
case 'update.policy':
|
|
516
535
|
if (config.settings.update) {
|
|
517
536
|
delete config.settings.update.policy;
|
package/dist/lib/env-config.js
CHANGED
package/dist/lib/env-proxy.js
CHANGED
|
@@ -23,6 +23,7 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
|
23
23
|
const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
|
|
24
24
|
const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
|
|
25
25
|
const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
|
|
26
|
+
const PORTAL_CLIENT_PREFIX = 'x';
|
|
26
27
|
const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
|
|
27
28
|
const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
|
|
28
29
|
const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
|
|
@@ -489,6 +490,7 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
489
490
|
const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
|
|
490
491
|
const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
|
|
491
492
|
const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
|
|
493
|
+
const fileAccessPath = `${context.appPublicPath}files/`;
|
|
492
494
|
const isRootMounted = context.appPublicPath === '/';
|
|
493
495
|
const appPublicPathRedirectBlock = isRootMounted
|
|
494
496
|
? ''
|
|
@@ -526,6 +528,22 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
526
528
|
` include ${context.snippetsDir}/proxy-location.conf;`,
|
|
527
529
|
' }',
|
|
528
530
|
'',
|
|
531
|
+
` location ^~ ${fileAccessPath} {`,
|
|
532
|
+
` proxy_pass ${context.backendUrl};`,
|
|
533
|
+
` include ${context.snippetsDir}/proxy-location.conf;`,
|
|
534
|
+
' }',
|
|
535
|
+
...(!isRootMounted
|
|
536
|
+
? [
|
|
537
|
+
'',
|
|
538
|
+
' location ^~ /files/ {',
|
|
539
|
+
` proxy_pass ${context.backendUrl};`,
|
|
540
|
+
` include ${context.snippetsDir}/proxy-location.conf;`,
|
|
541
|
+
' }',
|
|
542
|
+
]
|
|
543
|
+
: []),
|
|
544
|
+
'',
|
|
545
|
+
buildNginxPortalLocationBlock(context),
|
|
546
|
+
'',
|
|
529
547
|
` location = ${apiBasePathNoTrailingSlash} {`,
|
|
530
548
|
` return 308 ${context.apiBasePath}$is_args$args;`,
|
|
531
549
|
' }',
|
|
@@ -551,7 +569,7 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
551
569
|
` include ${context.snippetsDir}/spa-location.conf;`,
|
|
552
570
|
' }',
|
|
553
571
|
'',
|
|
554
|
-
` location
|
|
572
|
+
` location ${context.appPublicPath} {`,
|
|
555
573
|
` alias ${context.publicDir}/;`,
|
|
556
574
|
` try_files $uri /index-v1.html =404;`,
|
|
557
575
|
` include ${context.snippetsDir}/spa-location.conf;`,
|
|
@@ -560,6 +578,65 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
560
578
|
` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
|
|
561
579
|
].join('\n');
|
|
562
580
|
}
|
|
581
|
+
function buildNginxPortalRootPublicPath(appPublicPath) {
|
|
582
|
+
return appPublicPath === DEFAULT_APP_PUBLIC_PATH
|
|
583
|
+
? `/${PORTAL_CLIENT_PREFIX}/`
|
|
584
|
+
: `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
|
|
585
|
+
}
|
|
586
|
+
function buildNginxPortalLocationBlock(context) {
|
|
587
|
+
const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
|
|
588
|
+
const portalBasePathPattern = escapeRegExp(portalBasePath);
|
|
589
|
+
return [
|
|
590
|
+
` location ^~ ${portalBasePath}/apps/ {`,
|
|
591
|
+
' absolute_redirect off;',
|
|
592
|
+
'',
|
|
593
|
+
` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
|
|
594
|
+
` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
|
|
595
|
+
' }',
|
|
596
|
+
'',
|
|
597
|
+
` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
|
|
598
|
+
' return 404;',
|
|
599
|
+
' }',
|
|
600
|
+
'',
|
|
601
|
+
` root ${context.storageDir};`,
|
|
602
|
+
'',
|
|
603
|
+
' if ($portal_path = "") {',
|
|
604
|
+
' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
|
|
605
|
+
' }',
|
|
606
|
+
'',
|
|
607
|
+
' try_files',
|
|
608
|
+
' /portals/$subapp/$portal/dist/$portal_path',
|
|
609
|
+
' /portals/$subapp/$portal/dist/$portal_path/',
|
|
610
|
+
' /portals/$subapp/$portal/dist/index.html',
|
|
611
|
+
' =404;',
|
|
612
|
+
' }',
|
|
613
|
+
'',
|
|
614
|
+
` location ^~ ${portalBasePath}/ {`,
|
|
615
|
+
' absolute_redirect off;',
|
|
616
|
+
'',
|
|
617
|
+
` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
|
|
618
|
+
` return 308 ${portalBasePath}/$portal/$is_args$args;`,
|
|
619
|
+
' }',
|
|
620
|
+
'',
|
|
621
|
+
` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
|
|
622
|
+
' return 404;',
|
|
623
|
+
' }',
|
|
624
|
+
'',
|
|
625
|
+
` root ${context.storageDir};`,
|
|
626
|
+
'',
|
|
627
|
+
' if ($portal_path = "") {',
|
|
628
|
+
' rewrite ^ /portals/main/$portal/dist/index.html break;',
|
|
629
|
+
' }',
|
|
630
|
+
'',
|
|
631
|
+
' try_files',
|
|
632
|
+
' /portals/main/$portal/dist/$portal_path',
|
|
633
|
+
' /portals/main/$portal/dist/$portal_path/',
|
|
634
|
+
' /portals/main/$portal/dist/index.html',
|
|
635
|
+
' =404;',
|
|
636
|
+
' }',
|
|
637
|
+
'',
|
|
638
|
+
].join('\n');
|
|
639
|
+
}
|
|
563
640
|
function buildNginxRuntimeConfig(context, variant) {
|
|
564
641
|
return {
|
|
565
642
|
__webpack_public_path__: context.cdnBaseUrl,
|
|
@@ -593,6 +670,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
|
|
|
593
670
|
const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
|
|
594
671
|
const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
|
|
595
672
|
const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
|
|
673
|
+
const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
|
|
596
674
|
const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
|
|
597
675
|
const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
|
|
598
676
|
return {
|
|
@@ -617,6 +695,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
|
|
|
617
695
|
modernClientPrefix: source.settings.modernClientPrefix,
|
|
618
696
|
proxyHost,
|
|
619
697
|
snippetsDir: mappedSnippetsDir,
|
|
698
|
+
storageDir: mappedStorageDir,
|
|
620
699
|
uploadsDir: mappedUploadsDir,
|
|
621
700
|
v2PublicPath,
|
|
622
701
|
wsPath: source.settings.wsPath,
|
|
@@ -1098,6 +1177,7 @@ function buildCaddyContextCommentLines(siteAddress, context, publicDir) {
|
|
|
1098
1177
|
}
|
|
1099
1178
|
function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
1100
1179
|
const uploadsPath = `${context.appPublicPath}storage/uploads/`;
|
|
1180
|
+
const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
|
|
1101
1181
|
const distPathMatcher = toCaddyPathMatcher(context.distPath);
|
|
1102
1182
|
const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
|
|
1103
1183
|
const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
|
|
@@ -1168,7 +1248,19 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1168
1248
|
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1169
1249
|
' }',
|
|
1170
1250
|
'',
|
|
1171
|
-
' # Keep API and WS routes above the SPA fallbacks.',
|
|
1251
|
+
' # Keep file, API and WS routes above the SPA fallbacks.',
|
|
1252
|
+
` handle ${fileAccessPathMatcher} {`,
|
|
1253
|
+
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1254
|
+
' }',
|
|
1255
|
+
...(context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
|
|
1256
|
+
? []
|
|
1257
|
+
: [
|
|
1258
|
+
'',
|
|
1259
|
+
' handle /files/* {',
|
|
1260
|
+
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1261
|
+
' }',
|
|
1262
|
+
]),
|
|
1263
|
+
'',
|
|
1172
1264
|
` handle ${apiPathMatcher} {`,
|
|
1173
1265
|
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1174
1266
|
' }',
|
|
@@ -15,7 +15,7 @@ export function resolveManagedSetupState(value) {
|
|
|
15
15
|
export function isPreparedSetupState(value) {
|
|
16
16
|
return resolveManagedSetupState(value) === 'prepared';
|
|
17
17
|
}
|
|
18
|
-
export function buildInitAppEnvVarsFromConfig(config) {
|
|
18
|
+
export function buildInitAppEnvVarsFromConfig(config, options = {}) {
|
|
19
19
|
const out = {};
|
|
20
20
|
const put = (key, value) => {
|
|
21
21
|
const text = trimValue(value);
|
|
@@ -28,5 +28,10 @@ export function buildInitAppEnvVarsFromConfig(config) {
|
|
|
28
28
|
put('INIT_ROOT_EMAIL', config?.rootEmail);
|
|
29
29
|
put('INIT_ROOT_PASSWORD', config?.rootPassword);
|
|
30
30
|
put('INIT_ROOT_NICKNAME', config?.rootNickname);
|
|
31
|
+
if (options.includePortal !== false) {
|
|
32
|
+
put('INIT_PORTAL_TYPE', config?.portalType);
|
|
33
|
+
put('INIT_PORTAL_NAME', config?.portalName);
|
|
34
|
+
put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
|
|
35
|
+
}
|
|
31
36
|
return out;
|
|
32
37
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
const PORTAL_COMMAND_BASE_ENV_KEYS = [
|
|
10
|
+
'PATH',
|
|
11
|
+
'Path',
|
|
12
|
+
'PATHEXT',
|
|
13
|
+
'SystemRoot',
|
|
14
|
+
'WINDIR',
|
|
15
|
+
'ComSpec',
|
|
16
|
+
'HOME',
|
|
17
|
+
'USERPROFILE',
|
|
18
|
+
'TMPDIR',
|
|
19
|
+
'TEMP',
|
|
20
|
+
'TMP',
|
|
21
|
+
];
|
|
22
|
+
export function buildPortalCommandEnv(env = {}) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const key of PORTAL_COMMAND_BASE_ENV_KEYS) {
|
|
25
|
+
const value = process.env[key];
|
|
26
|
+
if (value) {
|
|
27
|
+
out[key] = value;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return { ...out, ...env };
|
|
31
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { executeApiRequest } from './api-client.js';
|
|
12
|
+
import { translateCli } from './cli-locale.js';
|
|
13
|
+
export const DEFAULT_PORTAL_GIT_PATH = '.';
|
|
14
|
+
const portalConfigText = (key, values, fallback) => translateCli(`commands.portalConfig.${key}`, values, { fallback });
|
|
15
|
+
const UPDATE_PORTAL_OPERATION = {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
pathTemplate: '/multiPortals:update',
|
|
18
|
+
hasBody: true,
|
|
19
|
+
bodyRequired: true,
|
|
20
|
+
parameters: [
|
|
21
|
+
{
|
|
22
|
+
name: 'filterByTk',
|
|
23
|
+
flagName: 'filterByTk',
|
|
24
|
+
in: 'query',
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
};
|
|
29
|
+
function trimValue(value) {
|
|
30
|
+
return String(value ?? '').trim();
|
|
31
|
+
}
|
|
32
|
+
function readObject(value) {
|
|
33
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
34
|
+
}
|
|
35
|
+
function validatePortalSourceStorage(value) {
|
|
36
|
+
const sourceStorage = trimValue(value) || 'nocobase';
|
|
37
|
+
if (sourceStorage === 'nocobase' || sourceStorage === 'git') {
|
|
38
|
+
return sourceStorage;
|
|
39
|
+
}
|
|
40
|
+
throw new Error(portalConfigText('errors.invalidSourceStorage', { value: sourceStorage }, `Invalid source storage "${sourceStorage}". Use "nocobase" or "git".`));
|
|
41
|
+
}
|
|
42
|
+
function isFullGitRemoteUrl(value) {
|
|
43
|
+
return /^(?:https?:\/\/|ssh:\/\/|file:\/\/|git@[^:]+:).+/.test(value);
|
|
44
|
+
}
|
|
45
|
+
function validateGitPath(value) {
|
|
46
|
+
const gitPath = trimValue(value);
|
|
47
|
+
if (!gitPath || path.isAbsolute(gitPath) || gitPath.split(/[\\/]+/).includes('..')) {
|
|
48
|
+
throw new Error(portalConfigText('errors.invalidGitPath', { value }, '--git-path must be a relative path inside the Git repository.'));
|
|
49
|
+
}
|
|
50
|
+
return gitPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
|
51
|
+
}
|
|
52
|
+
export function buildPortalConfig(options) {
|
|
53
|
+
const sourceStorage = validatePortalSourceStorage(options.sourceStorage ?? options.existingConfig?.sourceStorage);
|
|
54
|
+
const hasGitOption = Boolean(trimValue(options.gitRepo) || trimValue(options.gitBranch) || trimValue(options.gitPath));
|
|
55
|
+
if (sourceStorage === 'nocobase') {
|
|
56
|
+
if (hasGitOption) {
|
|
57
|
+
throw new Error(portalConfigText('errors.gitOptionsForNocobaseStorage', undefined, '--git-repo, --git-branch, and --git-path can only be used with --source-storage git.'));
|
|
58
|
+
}
|
|
59
|
+
return { sourceStorage };
|
|
60
|
+
}
|
|
61
|
+
const repo = trimValue(options.gitRepo) || options.existingConfig?.git?.repo || '';
|
|
62
|
+
if (!repo) {
|
|
63
|
+
throw new Error(portalConfigText('errors.gitRepoRequired', undefined, '--git-repo is required when --source-storage is git.'));
|
|
64
|
+
}
|
|
65
|
+
if (!isFullGitRemoteUrl(repo)) {
|
|
66
|
+
throw new Error(portalConfigText('errors.gitRepoInvalid', undefined, '--git-repo must be a full Git remote URL.'));
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
sourceStorage,
|
|
70
|
+
git: {
|
|
71
|
+
repo,
|
|
72
|
+
branch: trimValue(options.gitBranch) || options.existingConfig?.git?.branch || 'main',
|
|
73
|
+
path: validateGitPath(trimValue(options.gitPath) || options.existingConfig?.git?.path || DEFAULT_PORTAL_GIT_PATH),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function buildPortalConfigFromOptions(options, portal) {
|
|
78
|
+
const sourceOptions = readObject(options);
|
|
79
|
+
const git = readObject(sourceOptions.git);
|
|
80
|
+
return buildPortalConfig({
|
|
81
|
+
portal,
|
|
82
|
+
sourceStorage: trimValue(sourceOptions.sourceStorage) || 'nocobase',
|
|
83
|
+
gitRepo: trimValue(git.repo),
|
|
84
|
+
gitBranch: trimValue(git.branch),
|
|
85
|
+
gitPath: trimValue(git.path),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
export function mergePortalConfigIntoOptions(config, currentOptions) {
|
|
89
|
+
const nextOptions = {
|
|
90
|
+
...(currentOptions ?? {}),
|
|
91
|
+
sourceStorage: config.sourceStorage,
|
|
92
|
+
};
|
|
93
|
+
if (config.sourceStorage === 'git') {
|
|
94
|
+
nextOptions.git = config.git;
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
delete nextOptions.git;
|
|
98
|
+
}
|
|
99
|
+
return nextOptions;
|
|
100
|
+
}
|
|
101
|
+
export async function readPortalConfig(portalDir) {
|
|
102
|
+
const configPath = path.join(portalDir, 'portal.config.json');
|
|
103
|
+
const data = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
104
|
+
const config = readObject(data);
|
|
105
|
+
const git = readObject(config.git);
|
|
106
|
+
return buildPortalConfig({
|
|
107
|
+
portal: path.basename(portalDir),
|
|
108
|
+
sourceStorage: trimValue(config.sourceStorage),
|
|
109
|
+
gitRepo: trimValue(git.repo),
|
|
110
|
+
gitBranch: trimValue(git.branch),
|
|
111
|
+
gitPath: trimValue(git.path),
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export async function writePortalConfig(portalDir, config) {
|
|
115
|
+
await writeFile(path.join(portalDir, 'portal.config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
export async function syncPortalConfigToRemote(options) {
|
|
118
|
+
const apiRequest = options.apiRequest ?? executeApiRequest;
|
|
119
|
+
const response = await apiRequest({
|
|
120
|
+
cliVersion: options.cliVersion ?? '',
|
|
121
|
+
envName: options.envName,
|
|
122
|
+
flags: {
|
|
123
|
+
filterByTk: options.portal,
|
|
124
|
+
body: JSON.stringify({
|
|
125
|
+
options: mergePortalConfigIntoOptions(options.config, options.currentOptions),
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
128
|
+
operation: UPDATE_PORTAL_OPERATION,
|
|
129
|
+
});
|
|
130
|
+
if (!response.ok) {
|
|
131
|
+
throw new Error(portalConfigText('errors.updateFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal config update failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
import { mkdir, stat } from 'node:fs/promises';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { translateCli } from './cli-locale.js';
|
|
12
|
+
import { resolvePortalAppFromApiBaseUrl, resolvePortalStoragePath, validatePortalSlug, } from './portal-create.js';
|
|
13
|
+
import { buildPortalConfig, buildPortalConfigFromOptions, readPortalConfig, syncPortalConfigToRemote, writePortalConfig, } from './portal-config.js';
|
|
14
|
+
import { findPortalListItem } from './portal-info.js';
|
|
15
|
+
import { listPortalWorkspaces } from './portal-list.js';
|
|
16
|
+
const portalConfigureText = (key, values, fallback) => translateCli(`commands.portalConfigure.${key}`, values, { fallback });
|
|
17
|
+
function trimValue(value) {
|
|
18
|
+
return String(value ?? '').trim();
|
|
19
|
+
}
|
|
20
|
+
async function pathExists(target) {
|
|
21
|
+
try {
|
|
22
|
+
await stat(target);
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function hasConfigurationChange(options) {
|
|
30
|
+
return Boolean(options.sourceStorage !== undefined ||
|
|
31
|
+
trimValue(options.gitRepo) ||
|
|
32
|
+
trimValue(options.gitBranch) ||
|
|
33
|
+
trimValue(options.gitPath));
|
|
34
|
+
}
|
|
35
|
+
async function readExistingConfig(portalDir) {
|
|
36
|
+
try {
|
|
37
|
+
return await readPortalConfig(portalDir);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
const code = error.code;
|
|
41
|
+
if (code === 'ENOENT') {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function buildConfigFromRemoteOptions(params) {
|
|
48
|
+
if (params.options && Object.keys(params.options).length > 0) {
|
|
49
|
+
return buildPortalConfigFromOptions(params.options, params.portal);
|
|
50
|
+
}
|
|
51
|
+
if (!params.sourceStorage && !params.gitRepo && !params.gitBranch && !params.gitPath) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
return buildPortalConfig({
|
|
55
|
+
portal: params.portal,
|
|
56
|
+
sourceStorage: params.sourceStorage,
|
|
57
|
+
gitRepo: params.gitRepo,
|
|
58
|
+
gitBranch: params.gitBranch,
|
|
59
|
+
gitPath: params.gitPath,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export async function configurePortalWorkspace(options) {
|
|
63
|
+
if (!hasConfigurationChange(options)) {
|
|
64
|
+
throw new Error(portalConfigureText('errors.noChanges', undefined, 'No portal configuration changes were provided. Pass --source-storage or a --git-* flag.'));
|
|
65
|
+
}
|
|
66
|
+
const portal = validatePortalSlug(options.portal);
|
|
67
|
+
const apiBaseUrl = trimValue(options.env.apiBaseUrl);
|
|
68
|
+
const storagePath = resolvePortalStoragePath(options.env);
|
|
69
|
+
const { app } = resolvePortalAppFromApiBaseUrl(apiBaseUrl, options.env.config.appPublicPath);
|
|
70
|
+
const portalDir = path.join(storagePath, 'portals', app, portal);
|
|
71
|
+
if (!(await pathExists(portalDir))) {
|
|
72
|
+
throw new Error(portalConfigureText('errors.workspaceMissing', { portalDir, portal }, `Portal does not exist: ${portalDir}\nRun \`nb portal create ${portal}\` or \`nb portal pull ${portal}\` first.`));
|
|
73
|
+
}
|
|
74
|
+
const list = await listPortalWorkspaces({
|
|
75
|
+
env: options.env,
|
|
76
|
+
envName: options.envName,
|
|
77
|
+
cliVersion: options.cliVersion,
|
|
78
|
+
apiRequest: options.apiRequest,
|
|
79
|
+
});
|
|
80
|
+
const remoteItem = findPortalListItem(list.items, portal);
|
|
81
|
+
const existingConfig = (await readExistingConfig(portalDir)) ??
|
|
82
|
+
buildConfigFromRemoteOptions({
|
|
83
|
+
portal,
|
|
84
|
+
options: remoteItem?.options,
|
|
85
|
+
sourceStorage: remoteItem?.sourceStorage,
|
|
86
|
+
gitRepo: remoteItem?.gitRepo,
|
|
87
|
+
gitBranch: remoteItem?.gitBranch,
|
|
88
|
+
gitPath: remoteItem?.gitPath,
|
|
89
|
+
});
|
|
90
|
+
const config = buildPortalConfig({
|
|
91
|
+
portal,
|
|
92
|
+
sourceStorage: options.sourceStorage,
|
|
93
|
+
gitRepo: options.gitRepo,
|
|
94
|
+
gitBranch: options.gitBranch,
|
|
95
|
+
gitPath: options.gitPath,
|
|
96
|
+
existingConfig,
|
|
97
|
+
});
|
|
98
|
+
await mkdir(portalDir, { recursive: true });
|
|
99
|
+
await writePortalConfig(portalDir, config);
|
|
100
|
+
if (remoteItem) {
|
|
101
|
+
await syncPortalConfigToRemote({
|
|
102
|
+
portal,
|
|
103
|
+
config,
|
|
104
|
+
currentOptions: remoteItem.options,
|
|
105
|
+
envName: options.envName,
|
|
106
|
+
cliVersion: options.cliVersion,
|
|
107
|
+
apiRequest: options.apiRequest,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
app,
|
|
112
|
+
portal,
|
|
113
|
+
portalDir,
|
|
114
|
+
config,
|
|
115
|
+
remoteSynced: Boolean(remoteItem),
|
|
116
|
+
};
|
|
117
|
+
}
|