@nocobase/cli 2.2.0-test.16 → 3.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/app/start.js +1 -21
- package/dist/commands/config/set.js +1 -1
- package/dist/commands/init.js +12 -121
- package/dist/commands/install.js +39 -55
- package/dist/commands/portal/config.js +88 -0
- package/dist/commands/portal/create.js +7 -6
- package/dist/commands/portal/deploy.js +2 -2
- package/dist/commands/portal/destroy.js +6 -6
- package/dist/commands/portal/dev.js +3 -3
- package/dist/commands/portal/index.js +1 -1
- package/dist/commands/portal/info.js +3 -3
- package/dist/commands/portal/list.js +5 -5
- package/dist/commands/portal/pull.js +11 -4
- package/dist/commands/portal/push.js +4 -4
- package/dist/lib/api-client.js +7 -0
- package/dist/lib/env-auth.js +2 -2
- package/dist/lib/env-config.js +1 -1
- package/dist/lib/env-proxy.js +68 -6
- package/dist/lib/managed-env-file.js +58 -2
- package/dist/lib/managed-init-env.js +1 -1
- package/dist/lib/naming.js +9 -0
- package/dist/lib/portal-config.js +133 -0
- package/dist/lib/portal-configure.js +117 -0
- package/dist/lib/portal-create.js +24 -79
- package/dist/lib/portal-deploy.js +23 -15
- package/dist/lib/portal-destroy.js +3 -3
- package/dist/lib/portal-dev.js +3 -3
- package/dist/lib/portal-info.js +2 -2
- package/dist/lib/portal-list.js +32 -18
- package/dist/lib/portal-source.js +150 -43
- package/dist/lib/proxy-caddy.js +2 -0
- package/dist/lib/proxy-nginx.js +1 -0
- package/dist/locale/en-US.json +67 -43
- package/dist/locale/zh-CN.json +59 -35
- package/nocobase-ctl.config.json +111 -0
- package/package.json +4 -3
- package/dist/lib/portal-template.js +0 -190
package/dist/lib/env-proxy.js
CHANGED
|
@@ -20,6 +20,9 @@ const DEFAULT_API_BASE_PATH = '/api/';
|
|
|
20
20
|
const DEFAULT_WS_PATH = '/ws';
|
|
21
21
|
const DEFAULT_PLUGIN_STATICS_PATH = '/static/plugins/';
|
|
22
22
|
const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
|
|
23
|
+
const SETTINGS_CLIENT_PREFIX = 'settings';
|
|
24
|
+
const DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
|
|
25
|
+
const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
|
|
23
26
|
const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
|
|
24
27
|
const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
|
|
25
28
|
const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
|
|
@@ -67,7 +70,15 @@ function normalizeModernClientPrefix(value) {
|
|
|
67
70
|
const segment = String(value || '')
|
|
68
71
|
.trim()
|
|
69
72
|
.replace(/^\/+|\/+$/g, '');
|
|
70
|
-
|
|
73
|
+
const normalized = segment || DEFAULT_MODERN_CLIENT_PREFIX;
|
|
74
|
+
if (normalized === SETTINGS_CLIENT_PREFIX) {
|
|
75
|
+
throw new Error('APP_MODERN_CLIENT_PREFIX "settings" is reserved for the standalone Settings application.');
|
|
76
|
+
}
|
|
77
|
+
return normalized;
|
|
78
|
+
}
|
|
79
|
+
function normalizeAppClientEntryMode(value) {
|
|
80
|
+
const normalized = String(value || '').trim();
|
|
81
|
+
return APP_CLIENT_ENTRY_MODES.has(normalized) ? normalized : DEFAULT_APP_CLIENT_ENTRY_MODE;
|
|
71
82
|
}
|
|
72
83
|
function normalizeApiBasePath(value = DEFAULT_API_BASE_PATH) {
|
|
73
84
|
return resolveAppPublicPath(value);
|
|
@@ -275,6 +286,7 @@ export async function loadEnvProxySettings(runtime, options) {
|
|
|
275
286
|
wsPath: prefixRuntimePath(appPublicPath, envValues.WS_PATH || DEFAULT_WS_PATH),
|
|
276
287
|
pluginStaticsPath: prefixRuntimePath(appPublicPath, envValues.PLUGIN_STATICS_PATH || DEFAULT_PLUGIN_STATICS_PATH, { trailingSlash: true }),
|
|
277
288
|
modernClientPrefix: normalizeModernClientPrefix(envValues.APP_MODERN_CLIENT_PREFIX),
|
|
289
|
+
appClientEntryMode: normalizeAppClientEntryMode(envValues.APP_CLIENT_ENTRY_MODE),
|
|
278
290
|
cdnBaseUrl: trimValue(options?.cdnBaseUrl) ??
|
|
279
291
|
trimValue(runtime.env.envVars?.CDN_BASE_URL) ??
|
|
280
292
|
trimValue(envValues.CDN_BASE_URL),
|
|
@@ -299,7 +311,8 @@ function createManualProxyEnvSettings(input) {
|
|
|
299
311
|
pluginStaticsPath: prefixRuntimePath(appPublicPath, DEFAULT_PLUGIN_STATICS_PATH, {
|
|
300
312
|
trailingSlash: true,
|
|
301
313
|
}),
|
|
302
|
-
modernClientPrefix:
|
|
314
|
+
modernClientPrefix: normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX),
|
|
315
|
+
appClientEntryMode: normalizeAppClientEntryMode(process.env.APP_CLIENT_ENTRY_MODE),
|
|
303
316
|
cdnBaseUrl: trimValue(input.cdnBaseUrl),
|
|
304
317
|
apiClientStoragePrefix: DEFAULT_API_CLIENT_STORAGE_PREFIX,
|
|
305
318
|
apiClientStorageType: DEFAULT_API_CLIENT_STORAGE_TYPE,
|
|
@@ -491,6 +504,9 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
491
504
|
const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
|
|
492
505
|
const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
|
|
493
506
|
const fileAccessPath = `${context.appPublicPath}files/`;
|
|
507
|
+
const settingsAssetsPath = `${context.appPublicPath}settings/assets/`;
|
|
508
|
+
const settingsAssetsRoot = joinRuntimePath(context.distRootDir, `${context.activeVersion}/settings/assets`);
|
|
509
|
+
const settingsRoutePattern = `^${escapeRegExp(context.appPublicPath)}settings(?:/|$)`;
|
|
494
510
|
const isRootMounted = context.appPublicPath === '/';
|
|
495
511
|
const appPublicPathRedirectBlock = isRootMounted
|
|
496
512
|
? ''
|
|
@@ -563,6 +579,17 @@ function buildNginxManagedConfigBlock(context) {
|
|
|
563
579
|
` return 302 ${context.v2PublicPath}$is_args$args;`,
|
|
564
580
|
' }',
|
|
565
581
|
'',
|
|
582
|
+
` location ^~ ${settingsAssetsPath} {`,
|
|
583
|
+
` alias ${settingsAssetsRoot}/;`,
|
|
584
|
+
` include ${context.snippetsDir}/dist-location.conf;`,
|
|
585
|
+
' }',
|
|
586
|
+
'',
|
|
587
|
+
` location ~ ${settingsRoutePattern} {`,
|
|
588
|
+
` root ${context.publicDir};`,
|
|
589
|
+
` try_files $uri /index-settings.html =404;`,
|
|
590
|
+
` include ${context.snippetsDir}/spa-location.conf;`,
|
|
591
|
+
' }',
|
|
592
|
+
'',
|
|
566
593
|
` location ^~ ${context.v2PublicPath} {`,
|
|
567
594
|
` alias ${context.publicDir}/;`,
|
|
568
595
|
` try_files $uri /index-v2.html =404;`,
|
|
@@ -640,8 +667,9 @@ function buildNginxPortalLocationBlock(context) {
|
|
|
640
667
|
function buildNginxRuntimeConfig(context, variant) {
|
|
641
668
|
return {
|
|
642
669
|
__webpack_public_path__: context.cdnBaseUrl,
|
|
643
|
-
__nocobase_public_path__: variant === '
|
|
644
|
-
...(variant
|
|
670
|
+
__nocobase_public_path__: variant === 'v2' ? context.v2PublicPath : context.appPublicPath,
|
|
671
|
+
...(variant !== 'v1' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
|
|
672
|
+
__nocobase_app_client_entry_mode__: context.appClientEntryMode,
|
|
645
673
|
__nocobase_api_base_url__: context.apiBasePath,
|
|
646
674
|
__nocobase_api_client_storage_prefix__: context.apiClientStoragePrefix,
|
|
647
675
|
__nocobase_api_client_storage_type__: context.apiClientStorageType,
|
|
@@ -692,7 +720,9 @@ async function buildEnvProxyNginxRenderContext(source, options) {
|
|
|
692
720
|
esmCdnSuffix: source.settings.esmCdnSuffix,
|
|
693
721
|
indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
|
|
694
722
|
indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
|
|
723
|
+
indexSettingsPath: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }), options),
|
|
695
724
|
modernClientPrefix: source.settings.modernClientPrefix,
|
|
725
|
+
appClientEntryMode: source.settings.appClientEntryMode,
|
|
696
726
|
proxyHost,
|
|
697
727
|
snippetsDir: mappedSnippetsDir,
|
|
698
728
|
storageDir: mappedStorageDir,
|
|
@@ -811,16 +841,20 @@ async function buildNginxBundleFromSource(source, options) {
|
|
|
811
841
|
const mainTemplate = await readEnvProxyNginxAssetText('nocobase.conf.tpl');
|
|
812
842
|
const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
|
|
813
843
|
const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
|
|
814
|
-
const
|
|
844
|
+
const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
|
|
845
|
+
const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
|
|
815
846
|
readFile(sourceIndexV1Path, 'utf8'),
|
|
816
847
|
readFile(sourceIndexV2Path, 'utf8'),
|
|
848
|
+
readFile(sourceIndexSettingsPath, 'utf8'),
|
|
817
849
|
]);
|
|
818
850
|
const v1RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v1'));
|
|
819
851
|
const v2RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v2'));
|
|
852
|
+
const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'settings'));
|
|
820
853
|
const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
|
|
821
854
|
const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
|
|
822
855
|
const indexV1AssetPublicPath = context.cdnBaseUrl;
|
|
823
856
|
const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
|
|
857
|
+
const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
|
|
824
858
|
const appConfigIncludePath = await mapProxyPathFromCliRoot(path.join(resolveEnvProxyProviderRootDir('nginx', { scope: options?.scope }), '*', resolveEnvProxyFileSpec('nginx').appFilename), options);
|
|
825
859
|
const managedConfigBlock = buildNginxManagedConfigBlock(context);
|
|
826
860
|
const templateValues = {
|
|
@@ -844,6 +878,7 @@ async function buildNginxBundleFromSource(source, options) {
|
|
|
844
878
|
appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
|
|
845
879
|
indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
|
|
846
880
|
indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
|
|
881
|
+
indexSettingsPath: resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
|
|
847
882
|
mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
|
|
848
883
|
snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
|
|
849
884
|
appPublicPath: context.appPublicPath,
|
|
@@ -861,6 +896,7 @@ async function buildNginxBundleFromSource(source, options) {
|
|
|
861
896
|
}),
|
|
862
897
|
indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
|
|
863
898
|
indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
|
|
899
|
+
indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
|
|
864
900
|
};
|
|
865
901
|
}
|
|
866
902
|
export async function buildEnvProxyCaddyBundle(runtime, options) {
|
|
@@ -877,21 +913,26 @@ async function buildCaddyBundleFromSource(source, options) {
|
|
|
877
913
|
const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
|
|
878
914
|
const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
|
|
879
915
|
const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
|
|
880
|
-
const
|
|
916
|
+
const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
|
|
917
|
+
const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
|
|
881
918
|
readFile(sourceIndexV1Path, 'utf8'),
|
|
882
919
|
readFile(sourceIndexV2Path, 'utf8'),
|
|
920
|
+
readFile(sourceIndexSettingsPath, 'utf8'),
|
|
883
921
|
]);
|
|
884
922
|
const v1RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v1'));
|
|
885
923
|
const v2RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v2'));
|
|
924
|
+
const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'settings'));
|
|
886
925
|
const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
|
|
887
926
|
const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
|
|
888
927
|
const indexV1AssetPublicPath = context.cdnBaseUrl;
|
|
889
928
|
const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
|
|
929
|
+
const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
|
|
890
930
|
const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
|
|
891
931
|
const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
|
|
892
932
|
const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
|
|
893
933
|
const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
|
|
894
934
|
const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
|
|
935
|
+
activeVersion: context.activeVersion,
|
|
895
936
|
appPublicPath: context.appPublicPath,
|
|
896
937
|
apiBasePath: context.apiBasePath,
|
|
897
938
|
apiPort: context.apiPort,
|
|
@@ -912,6 +953,7 @@ async function buildCaddyBundleFromSource(source, options) {
|
|
|
912
953
|
appConfigPath,
|
|
913
954
|
indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
|
|
914
955
|
indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
|
|
956
|
+
indexSettingsPath: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
|
|
915
957
|
mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
|
|
916
958
|
appPublicPath: context.appPublicPath,
|
|
917
959
|
apiBasePath: context.apiBasePath,
|
|
@@ -925,6 +967,7 @@ async function buildCaddyBundleFromSource(source, options) {
|
|
|
925
967
|
mainConfigContent: await buildEnvProxyMainConfig({ provider: 'caddy', scope: options?.scope }),
|
|
926
968
|
indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
|
|
927
969
|
indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
|
|
970
|
+
indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
|
|
928
971
|
};
|
|
929
972
|
}
|
|
930
973
|
async function pathExists(candidate) {
|
|
@@ -1179,6 +1222,9 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1179
1222
|
const uploadsPath = `${context.appPublicPath}storage/uploads/`;
|
|
1180
1223
|
const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
|
|
1181
1224
|
const distPathMatcher = toCaddyPathMatcher(context.distPath);
|
|
1225
|
+
const settingsAssetsPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}settings/assets/`);
|
|
1226
|
+
const settingsAssetsRoot = joinRuntimePath(context.distClientRoot, `${context.activeVersion}/settings/assets`);
|
|
1227
|
+
const settingsRoutePattern = `^${escapeRegExp(context.appPublicPath)}settings(?:/.*)?$`;
|
|
1182
1228
|
const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
|
|
1183
1229
|
const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
|
|
1184
1230
|
const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
|
|
@@ -1236,6 +1282,12 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1236
1282
|
' file_server',
|
|
1237
1283
|
' }',
|
|
1238
1284
|
'',
|
|
1285
|
+
` handle_path ${settingsAssetsPathMatcher} {`,
|
|
1286
|
+
` root * ${settingsAssetsRoot}`,
|
|
1287
|
+
' header Cache-Control "public, max-age=31536000, immutable"',
|
|
1288
|
+
' file_server',
|
|
1289
|
+
' }',
|
|
1290
|
+
'',
|
|
1239
1291
|
' @oauth path_regexp oauth ^/\\.well-known/oauth-authorization-server/(.+)$',
|
|
1240
1292
|
' handle @oauth {',
|
|
1241
1293
|
' rewrite * /{re.oauth.1}/.well-known/oauth-authorization-server',
|
|
@@ -1269,6 +1321,15 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
|
|
|
1269
1321
|
` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
|
|
1270
1322
|
' }',
|
|
1271
1323
|
'',
|
|
1324
|
+
` @settingsRoute path_regexp settingsRoute ${settingsRoutePattern}`,
|
|
1325
|
+
' handle @settingsRoute {',
|
|
1326
|
+
` root * ${publicDir}`,
|
|
1327
|
+
' header Cache-Control "no-store, no-cache, must-revalidate"',
|
|
1328
|
+
' header X-Robots-Tag "noindex, nofollow"',
|
|
1329
|
+
' try_files {path} /index-settings.html',
|
|
1330
|
+
' file_server',
|
|
1331
|
+
' }',
|
|
1332
|
+
'',
|
|
1272
1333
|
' # Keep the v2 SPA route above the fallback SPA route.',
|
|
1273
1334
|
` handle_path ${toCaddyPathMatcher(context.v2PublicPath)} {`,
|
|
1274
1335
|
` root * ${publicDir}`,
|
|
@@ -1341,6 +1402,7 @@ async function buildEnvProxyRenderState(runtime, options) {
|
|
|
1341
1402
|
: await mapProxyPathFromCliRoot(distClientRoot, options);
|
|
1342
1403
|
const provider = resolveProxyProviderName(options?.provider);
|
|
1343
1404
|
const templateContext = {
|
|
1405
|
+
activeVersion: runtimeVersion,
|
|
1344
1406
|
appPublicPath: settings.appPublicPath,
|
|
1345
1407
|
apiBasePath: settings.apiBasePath,
|
|
1346
1408
|
apiPort,
|
|
@@ -6,11 +6,17 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
10
10
|
import path from 'node:path';
|
|
11
|
+
import { resolveEnvKind } from './auth-store.js';
|
|
11
12
|
import { resolveConfiguredEnvPath } from './cli-home.js';
|
|
12
|
-
import { resolveDockerEnvFileArg } from "./docker-env-file.js";
|
|
13
|
+
import { resolveDockerEnvFileArg, resolveDockerEnvFilePath } from "./docker-env-file.js";
|
|
13
14
|
import { resolveConfiguredAppPath } from './env-paths.js';
|
|
15
|
+
export const DEFAULT_MANAGED_ENV_FILE_VALUES = {
|
|
16
|
+
APP_DISCOVERY_ADAPTER: 'local',
|
|
17
|
+
APP_PROCESS_ADAPTER: 'local',
|
|
18
|
+
APP_CLIENT_ENTRY_MODE: 'modern-only',
|
|
19
|
+
};
|
|
14
20
|
function trimValue(value) {
|
|
15
21
|
const text = String(value ?? '').trim();
|
|
16
22
|
return text || undefined;
|
|
@@ -68,6 +74,56 @@ export function resolveManagedLocalEnvFilePath(runtime) {
|
|
|
68
74
|
}
|
|
69
75
|
return normalizeEnvFilePath(path.join(runtime.projectRoot, '.env'));
|
|
70
76
|
}
|
|
77
|
+
export function resolveManagedEnvFilePathFromConfig(envName, config) {
|
|
78
|
+
const kind = config?.kind ?? resolveEnvKind(config);
|
|
79
|
+
if (kind === 'docker') {
|
|
80
|
+
const filePath = resolveDockerEnvFilePath(envName, config);
|
|
81
|
+
return filePath ? normalizeEnvFilePath(filePath) : undefined;
|
|
82
|
+
}
|
|
83
|
+
if (kind !== 'local') {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const explicitEnvFile = trimValue(config?.envFile);
|
|
87
|
+
if (explicitEnvFile) {
|
|
88
|
+
return normalizeEnvFilePath(resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile);
|
|
89
|
+
}
|
|
90
|
+
const configuredAppPath = resolveConfiguredAppPath(config);
|
|
91
|
+
if (configuredAppPath) {
|
|
92
|
+
return normalizeEnvFilePath(path.join(configuredAppPath, '.env'));
|
|
93
|
+
}
|
|
94
|
+
const configuredAppRootPath = trimValue(config?.appRootPath);
|
|
95
|
+
if (configuredAppRootPath) {
|
|
96
|
+
const appRootPath = resolveConfiguredEnvPath(configuredAppRootPath) ?? configuredAppRootPath;
|
|
97
|
+
return normalizeEnvFilePath(path.basename(appRootPath) === 'source' ? path.resolve(appRootPath, '..', '.env') : path.join(appRootPath, '.env'));
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
export async function ensureManagedEnvFileDefaults(envName, config, defaults = DEFAULT_MANAGED_ENV_FILE_VALUES) {
|
|
102
|
+
const envFilePath = resolveManagedEnvFilePathFromConfig(envName, config);
|
|
103
|
+
if (!envFilePath) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
let content = '';
|
|
107
|
+
try {
|
|
108
|
+
content = await readFile(envFilePath, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
|
|
112
|
+
if (code !== 'ENOENT') {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const existing = parseSimpleEnvFile(content);
|
|
117
|
+
const missingEntries = Object.entries(defaults).filter(([key, value]) => trimValue(value) && !existing[key]);
|
|
118
|
+
if (missingEntries.length === 0) {
|
|
119
|
+
return envFilePath;
|
|
120
|
+
}
|
|
121
|
+
const separator = content && !content.endsWith('\n') ? '\n' : '';
|
|
122
|
+
const nextContent = `${content}${separator}${missingEntries.map(([key, value]) => `${key}=${value}`).join('\n')}\n`;
|
|
123
|
+
await mkdir(path.dirname(envFilePath), { recursive: true });
|
|
124
|
+
await writeFile(envFilePath, nextContent, 'utf8');
|
|
125
|
+
return envFilePath;
|
|
126
|
+
}
|
|
71
127
|
export async function resolveManagedRuntimeEnvFilePath(runtime) {
|
|
72
128
|
if (runtime.kind === 'local') {
|
|
73
129
|
return resolveManagedLocalEnvFilePath(runtime);
|
|
@@ -29,7 +29,7 @@ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
|
|
|
29
29
|
put('INIT_ROOT_PASSWORD', config?.rootPassword);
|
|
30
30
|
put('INIT_ROOT_NICKNAME', config?.rootNickname);
|
|
31
31
|
if (options.includePortal !== false) {
|
|
32
|
-
put('
|
|
32
|
+
put('INIT_PORTAL_TYPE', config?.portalType);
|
|
33
33
|
put('INIT_PORTAL_NAME', config?.portalName);
|
|
34
34
|
put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
|
|
35
35
|
}
|
package/dist/lib/naming.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
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
|
+
*/
|
|
1
9
|
import path from 'node:path';
|
|
2
10
|
export function toKebabCase(value) {
|
|
3
11
|
return value
|
|
12
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
|
|
4
13
|
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
5
14
|
.replace(/[^a-zA-Z0-9]+/g, '-')
|
|
6
15
|
.replace(/-+/g, '-')
|
|
@@ -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
|
+
}
|