@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.
Files changed (37) hide show
  1. package/dist/commands/app/start.js +1 -21
  2. package/dist/commands/config/set.js +1 -1
  3. package/dist/commands/init.js +12 -121
  4. package/dist/commands/install.js +39 -55
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +7 -6
  7. package/dist/commands/portal/deploy.js +2 -2
  8. package/dist/commands/portal/destroy.js +6 -6
  9. package/dist/commands/portal/dev.js +3 -3
  10. package/dist/commands/portal/index.js +1 -1
  11. package/dist/commands/portal/info.js +3 -3
  12. package/dist/commands/portal/list.js +5 -5
  13. package/dist/commands/portal/pull.js +11 -4
  14. package/dist/commands/portal/push.js +4 -4
  15. package/dist/lib/api-client.js +7 -0
  16. package/dist/lib/env-auth.js +2 -2
  17. package/dist/lib/env-config.js +1 -1
  18. package/dist/lib/env-proxy.js +68 -6
  19. package/dist/lib/managed-env-file.js +58 -2
  20. package/dist/lib/managed-init-env.js +1 -1
  21. package/dist/lib/naming.js +9 -0
  22. package/dist/lib/portal-config.js +133 -0
  23. package/dist/lib/portal-configure.js +117 -0
  24. package/dist/lib/portal-create.js +24 -79
  25. package/dist/lib/portal-deploy.js +23 -15
  26. package/dist/lib/portal-destroy.js +3 -3
  27. package/dist/lib/portal-dev.js +3 -3
  28. package/dist/lib/portal-info.js +2 -2
  29. package/dist/lib/portal-list.js +32 -18
  30. package/dist/lib/portal-source.js +150 -43
  31. package/dist/lib/proxy-caddy.js +2 -0
  32. package/dist/lib/proxy-nginx.js +1 -0
  33. package/dist/locale/en-US.json +67 -43
  34. package/dist/locale/zh-CN.json +59 -35
  35. package/nocobase-ctl.config.json +111 -0
  36. package/package.json +4 -3
  37. package/dist/lib/portal-template.js +0 -190
@@ -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
- return segment || DEFAULT_MODERN_CLIENT_PREFIX;
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: DEFAULT_MODERN_CLIENT_PREFIX,
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 === 'v1' ? context.appPublicPath : context.v2PublicPath,
644
- ...(variant === 'v2' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
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 [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
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 [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
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('INIT_DEVELOPMENT_MODE', config?.developmentMode);
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
  }
@@ -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
+ }