@nocobase/cli 2.3.0-beta.9 → 2.4.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.
Files changed (56) hide show
  1. package/dist/commands/api/swagger/get.js +96 -0
  2. package/dist/commands/api/swagger/index.js +20 -0
  3. package/dist/commands/api/swagger/list.js +92 -0
  4. package/dist/commands/config/set.js +1 -0
  5. package/dist/commands/env/add.js +6 -0
  6. package/dist/commands/env/update.js +13 -0
  7. package/dist/commands/init.js +51 -5
  8. package/dist/commands/install.js +60 -3
  9. package/dist/commands/portal/config.js +99 -0
  10. package/dist/commands/portal/create.js +96 -0
  11. package/dist/commands/portal/deploy.js +81 -0
  12. package/dist/commands/portal/destroy.js +126 -0
  13. package/dist/commands/portal/dev.js +73 -0
  14. package/dist/commands/portal/index.js +20 -0
  15. package/dist/commands/portal/info.js +82 -0
  16. package/dist/commands/portal/list.js +98 -0
  17. package/dist/commands/portal/pull.js +113 -0
  18. package/dist/commands/portal/push.js +79 -0
  19. package/dist/commands/source/dev.js +1 -1
  20. package/dist/lib/api-client.js +35 -9
  21. package/dist/lib/app-client-entry-mode.js +28 -0
  22. package/dist/lib/app-managed-resources.js +2 -0
  23. package/dist/lib/auth-store.js +55 -2
  24. package/dist/lib/bootstrap.js +3 -1
  25. package/dist/lib/cli-config.js +20 -1
  26. package/dist/lib/env-auth.js +2 -36
  27. package/dist/lib/env-command-config.js +1 -0
  28. package/dist/lib/env-config.js +11 -0
  29. package/dist/lib/env-portal-config.js +30 -0
  30. package/dist/lib/env-proxy.js +154 -7
  31. package/dist/lib/managed-env-file.js +119 -9
  32. package/dist/lib/managed-init-env.js +4 -1
  33. package/dist/lib/portal-build-html.js +27 -0
  34. package/dist/lib/portal-command-env.js +31 -0
  35. package/dist/lib/portal-config.js +119 -0
  36. package/dist/lib/portal-configure.js +110 -0
  37. package/dist/lib/portal-create.js +515 -0
  38. package/dist/lib/portal-deploy.js +266 -0
  39. package/dist/lib/portal-destroy.js +114 -0
  40. package/dist/lib/portal-dev.js +78 -0
  41. package/dist/lib/portal-env-files.js +54 -0
  42. package/dist/lib/portal-info.js +28 -0
  43. package/dist/lib/portal-list.js +205 -0
  44. package/dist/lib/portal-path-safety.js +76 -0
  45. package/dist/lib/portal-source.js +692 -0
  46. package/dist/lib/prompt-catalog-core.js +2 -2
  47. package/dist/lib/prompt-catalog-terminal.js +4 -5
  48. package/dist/lib/prompt-web-ui.js +12 -6
  49. package/dist/lib/proxy-caddy.js +2 -0
  50. package/dist/lib/proxy-nginx.js +1 -0
  51. package/dist/lib/run-npm.js +85 -20
  52. package/dist/lib/swagger-command.js +52 -0
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +245 -1
  55. package/dist/locale/zh-CN.json +245 -1
  56. package/package.json +5 -2
@@ -27,40 +27,6 @@ const DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
27
27
  function normalizeBaseUrl(baseUrl) {
28
28
  return baseUrl.replace(/\/+$/, '');
29
29
  }
30
- function buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl) {
31
- const url = new URL(apiBaseUrl);
32
- const subappMatch = url.pathname.match(/^(.*)\/api\/__app\/([^/]+)\/?$/);
33
- if (subappMatch) {
34
- const publicPath = (subappMatch[1] || '').replace(/\/+$/, '');
35
- return `${publicPath}/apps/${subappMatch[2]}/idpOAuth/device`;
36
- }
37
- const appMatch = url.pathname.match(/^(.*)\/api\/?$/);
38
- if (appMatch) {
39
- const publicPath = (appMatch[1] || '').replace(/\/+$/, '');
40
- return `${publicPath}/idpOAuth/device`;
41
- }
42
- return undefined;
43
- }
44
- export function resolveDeviceVerificationUrlForApiBaseUrl(verificationUrl, apiBaseUrl) {
45
- try {
46
- const devicePath = buildDeviceVerificationPathFromApiBaseUrl(apiBaseUrl);
47
- if (!devicePath) {
48
- return verificationUrl;
49
- }
50
- const originalUrl = new URL(verificationUrl);
51
- if (!originalUrl.pathname.endsWith('/idpOAuth/device')) {
52
- return verificationUrl;
53
- }
54
- const publicUrl = new URL(apiBaseUrl);
55
- publicUrl.pathname = devicePath;
56
- publicUrl.search = originalUrl.search;
57
- publicUrl.hash = originalUrl.hash;
58
- return publicUrl.toString();
59
- }
60
- catch {
61
- return verificationUrl;
62
- }
63
- }
64
30
  export function getOauthMetadataUrl(baseUrl) {
65
31
  return `${normalizeBaseUrl(baseUrl)}/.well-known/oauth-authorization-server`;
66
32
  }
@@ -973,7 +939,7 @@ export async function resolveServerRequestTarget(options) {
973
939
  : `Use --api-base-url or run \`nb init --ui --env ${envName}\` first.`,
974
940
  ].join('\n'));
975
941
  }
976
- return { baseUrl, token };
942
+ return { baseUrl, token, envName };
977
943
  }
978
944
  export async function authenticateEnvWithBasic(options) {
979
945
  const envName = options.envName ?? (await getCurrentEnvName({ scope: options.scope }));
@@ -1051,7 +1017,7 @@ async function authenticateEnvWithOauthDevice(options) {
1051
1017
  baseUrl: options.baseUrl,
1052
1018
  });
1053
1019
  stopTask();
1054
- const verificationUrl = resolveDeviceVerificationUrlForApiBaseUrl(deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri, options.baseUrl);
1020
+ const verificationUrl = deviceAuthorization.verification_uri_complete || deviceAuthorization.verification_uri;
1055
1021
  const browser = await browserOpener(verificationUrl);
1056
1022
  cleanupBrowserOpenTarget = browser.cleanup;
1057
1023
  if (!browser.opened) {
@@ -19,6 +19,7 @@ export const ENV_STRING_CONFIG_FLAG_MAP = {
19
19
  'app-public-path': 'appPublicPath',
20
20
  'cdn-base-url': 'cdnBaseUrl',
21
21
  'env-file': 'envFile',
22
+ 'app-client-entry-mode': 'appClientEntryMode',
22
23
  'app-port': 'appPort',
23
24
  'app-key': 'appKey',
24
25
  timezone: 'timezone',
@@ -6,8 +6,10 @@
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 { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
9
10
  import { normalizeEnvProxyConfig } from './env-proxy-config.js';
10
11
  import { resolveAppPublicPath } from './app-public-path.js';
12
+ import { normalizeEnvPortalsConfig } from './env-portal-config.js';
11
13
  const STRING_ENV_CONFIG_KEYS = [
12
14
  'source',
13
15
  'downloadVersion',
@@ -36,6 +38,7 @@ const STRING_ENV_CONFIG_KEYS = [
36
38
  'dbSchema',
37
39
  'dbTablePrefix',
38
40
  'lang',
41
+ 'portalTemplate',
39
42
  'rootUsername',
40
43
  'rootEmail',
41
44
  'rootPassword',
@@ -83,6 +86,10 @@ export function buildStoredEnvConfig(input) {
83
86
  envConfig[key] = key === 'appPublicPath' ? resolveAppPublicPath(value) : value;
84
87
  }
85
88
  }
89
+ const appClientEntryMode = normalizeAppClientEntryMode(input.appClientEntryMode);
90
+ if (appClientEntryMode) {
91
+ envConfig.appClientEntryMode = appClientEntryMode;
92
+ }
86
93
  const setupState = resolveSetupState(input.setupState);
87
94
  if (setupState) {
88
95
  envConfig.setupState = setupState;
@@ -115,5 +122,9 @@ export function buildStoredEnvConfig(input) {
115
122
  if (proxy) {
116
123
  envConfig.proxy = proxy;
117
124
  }
125
+ const portals = normalizeEnvPortalsConfig(input.portals);
126
+ if (portals) {
127
+ envConfig.portals = portals;
128
+ }
118
129
  return envConfig;
119
130
  }
@@ -0,0 +1,30 @@
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
+ function trimValue(value) {
10
+ return String(value ?? '').trim();
11
+ }
12
+ function readRecord(value) {
13
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
14
+ }
15
+ export function normalizeEnvPortalsConfig(value) {
16
+ const input = readRecord(value);
17
+ const portals = {};
18
+ for (const [portal, rawEntry] of Object.entries(input)) {
19
+ const portalName = trimValue(portal);
20
+ if (!portalName) {
21
+ continue;
22
+ }
23
+ const entry = readRecord(rawEntry);
24
+ const portalPath = trimValue(entry.path);
25
+ if (portalPath) {
26
+ portals[portalName] = { path: portalPath };
27
+ }
28
+ }
29
+ return Object.keys(portals).length > 0 ? portals : undefined;
30
+ }
@@ -20,9 +20,13 @@ 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', 'settings-default']);
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';
29
+ const PORTAL_CLIENT_PREFIX = 'x';
26
30
  const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
27
31
  const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
28
32
  const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
@@ -66,7 +70,15 @@ function normalizeModernClientPrefix(value) {
66
70
  const segment = String(value || '')
67
71
  .trim()
68
72
  .replace(/^\/+|\/+$/g, '');
69
- 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;
70
82
  }
71
83
  function normalizeApiBasePath(value = DEFAULT_API_BASE_PATH) {
72
84
  return resolveAppPublicPath(value);
@@ -274,6 +286,7 @@ export async function loadEnvProxySettings(runtime, options) {
274
286
  wsPath: prefixRuntimePath(appPublicPath, envValues.WS_PATH || DEFAULT_WS_PATH),
275
287
  pluginStaticsPath: prefixRuntimePath(appPublicPath, envValues.PLUGIN_STATICS_PATH || DEFAULT_PLUGIN_STATICS_PATH, { trailingSlash: true }),
276
288
  modernClientPrefix: normalizeModernClientPrefix(envValues.APP_MODERN_CLIENT_PREFIX),
289
+ appClientEntryMode: normalizeAppClientEntryMode(envValues.APP_CLIENT_ENTRY_MODE),
277
290
  cdnBaseUrl: trimValue(options?.cdnBaseUrl) ??
278
291
  trimValue(runtime.env.envVars?.CDN_BASE_URL) ??
279
292
  trimValue(envValues.CDN_BASE_URL),
@@ -298,7 +311,8 @@ function createManualProxyEnvSettings(input) {
298
311
  pluginStaticsPath: prefixRuntimePath(appPublicPath, DEFAULT_PLUGIN_STATICS_PATH, {
299
312
  trailingSlash: true,
300
313
  }),
301
- modernClientPrefix: DEFAULT_MODERN_CLIENT_PREFIX,
314
+ modernClientPrefix: normalizeModernClientPrefix(process.env.APP_MODERN_CLIENT_PREFIX),
315
+ appClientEntryMode: normalizeAppClientEntryMode(process.env.APP_CLIENT_ENTRY_MODE),
302
316
  cdnBaseUrl: trimValue(input.cdnBaseUrl),
303
317
  apiClientStoragePrefix: DEFAULT_API_CLIENT_STORAGE_PREFIX,
304
318
  apiClientStorageType: DEFAULT_API_CLIENT_STORAGE_TYPE,
@@ -490,6 +504,9 @@ function buildNginxManagedConfigBlock(context) {
490
504
  const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
491
505
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
492
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(?:/|$)`;
493
510
  const isRootMounted = context.appPublicPath === '/';
494
511
  const appPublicPathRedirectBlock = isRootMounted
495
512
  ? ''
@@ -554,6 +571,8 @@ function buildNginxManagedConfigBlock(context) {
554
571
  ]
555
572
  : []),
556
573
  '',
574
+ buildNginxPortalLocationBlock(context),
575
+ '',
557
576
  ` location = ${apiBasePathNoTrailingSlash} {`,
558
577
  ` return 308 ${context.apiBasePath}$is_args$args;`,
559
578
  ' }',
@@ -573,13 +592,24 @@ function buildNginxManagedConfigBlock(context) {
573
592
  ` return 302 ${context.v2PublicPath}$is_args$args;`,
574
593
  ' }',
575
594
  '',
595
+ ` location ^~ ${settingsAssetsPath} {`,
596
+ ` alias ${settingsAssetsRoot}/;`,
597
+ ` include ${context.snippetsDir}/dist-location.conf;`,
598
+ ' }',
599
+ '',
600
+ ` location ~ ${settingsRoutePattern} {`,
601
+ ` root ${context.publicDir};`,
602
+ ` try_files $uri /index-settings.html =404;`,
603
+ ` include ${context.snippetsDir}/spa-location.conf;`,
604
+ ' }',
605
+ '',
576
606
  ` location ^~ ${context.v2PublicPath} {`,
577
607
  ` alias ${context.publicDir}/;`,
578
608
  ` try_files $uri /index-v2.html =404;`,
579
609
  ` include ${context.snippetsDir}/spa-location.conf;`,
580
610
  ' }',
581
611
  '',
582
- ` location ^~ ${context.appPublicPath} {`,
612
+ ` location ${context.appPublicPath} {`,
583
613
  ` alias ${context.publicDir}/;`,
584
614
  ` try_files $uri /index-v1.html =404;`,
585
615
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -588,11 +618,83 @@ function buildNginxManagedConfigBlock(context) {
588
618
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
589
619
  ].join('\n');
590
620
  }
621
+ function buildPortalRootPublicPath(appPublicPath) {
622
+ return appPublicPath === DEFAULT_APP_PUBLIC_PATH
623
+ ? `/${PORTAL_CLIENT_PREFIX}/`
624
+ : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
625
+ }
626
+ function buildNginxPortalLocationBlock(context) {
627
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
628
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
629
+ return [
630
+ ` location = ${portalBasePath} {`,
631
+ ' absolute_redirect off;',
632
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
633
+ ' }',
634
+ '',
635
+ ` location = ${portalBasePath}/ {`,
636
+ ' absolute_redirect off;',
637
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
638
+ ' }',
639
+ '',
640
+ ` location ^~ ${portalBasePath}/apps/ {`,
641
+ ' absolute_redirect off;',
642
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {`,
643
+ ` return 302 ${context.v2PublicPath}apps/$subapp/$is_args$args;`,
644
+ ' }',
645
+ '',
646
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
647
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
648
+ ' }',
649
+ '',
650
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
651
+ ' return 404;',
652
+ ' }',
653
+ '',
654
+ ` root ${context.storageDir};`,
655
+ '',
656
+ ' if ($portal_path = "") {',
657
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
658
+ ' }',
659
+ '',
660
+ ' try_files',
661
+ ' /portals/$subapp/$portal/dist/$portal_path',
662
+ ' /portals/$subapp/$portal/dist/$portal_path/',
663
+ ' /portals/$subapp/$portal/dist/index.html',
664
+ ' =404;',
665
+ ' }',
666
+ '',
667
+ ` location ^~ ${portalBasePath}/ {`,
668
+ ' absolute_redirect off;',
669
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
670
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
671
+ ' }',
672
+ '',
673
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
674
+ ' return 404;',
675
+ ' }',
676
+ '',
677
+ ` root ${context.storageDir};`,
678
+ '',
679
+ ' if ($portal_path = "") {',
680
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
681
+ ' }',
682
+ '',
683
+ ' try_files',
684
+ ' /portals/main/$portal/dist/$portal_path',
685
+ ' /portals/main/$portal/dist/$portal_path/',
686
+ ' /portals/main/$portal/dist/index.html',
687
+ ' =404;',
688
+ ' }',
689
+ '',
690
+ ].join('\n');
691
+ }
591
692
  function buildNginxRuntimeConfig(context, variant) {
592
693
  return {
593
694
  __webpack_public_path__: context.cdnBaseUrl,
594
- __nocobase_public_path__: variant === 'v1' ? context.appPublicPath : context.v2PublicPath,
595
- ...(variant === 'v2' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
695
+ __nocobase_public_path__: variant === 'v2' ? context.v2PublicPath : context.appPublicPath,
696
+ ...(variant !== 'v1' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
697
+ __nocobase_app_client_entry_mode__: context.appClientEntryMode,
596
698
  __nocobase_api_base_url__: context.apiBasePath,
597
699
  __nocobase_api_client_storage_prefix__: context.apiClientStoragePrefix,
598
700
  __nocobase_api_client_storage_type__: context.apiClientStorageType,
@@ -621,6 +723,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
621
723
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
622
724
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
623
725
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
726
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
624
727
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
625
728
  const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
626
729
  return {
@@ -642,9 +745,12 @@ async function buildEnvProxyNginxRenderContext(source, options) {
642
745
  esmCdnSuffix: source.settings.esmCdnSuffix,
643
746
  indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
644
747
  indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
748
+ indexSettingsPath: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }), options),
645
749
  modernClientPrefix: source.settings.modernClientPrefix,
750
+ appClientEntryMode: source.settings.appClientEntryMode,
646
751
  proxyHost,
647
752
  snippetsDir: mappedSnippetsDir,
753
+ storageDir: mappedStorageDir,
648
754
  uploadsDir: mappedUploadsDir,
649
755
  v2PublicPath,
650
756
  wsPath: source.settings.wsPath,
@@ -760,16 +866,20 @@ async function buildNginxBundleFromSource(source, options) {
760
866
  const mainTemplate = await readEnvProxyNginxAssetText('nocobase.conf.tpl');
761
867
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
762
868
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
763
- const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
869
+ const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
870
+ const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
764
871
  readFile(sourceIndexV1Path, 'utf8'),
765
872
  readFile(sourceIndexV2Path, 'utf8'),
873
+ readFile(sourceIndexSettingsPath, 'utf8'),
766
874
  ]);
767
875
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v1'));
768
876
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v2'));
877
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'settings'));
769
878
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
770
879
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
771
880
  const indexV1AssetPublicPath = context.cdnBaseUrl;
772
881
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
882
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
773
883
  const appConfigIncludePath = await mapProxyPathFromCliRoot(path.join(resolveEnvProxyProviderRootDir('nginx', { scope: options?.scope }), '*', resolveEnvProxyFileSpec('nginx').appFilename), options);
774
884
  const managedConfigBlock = buildNginxManagedConfigBlock(context);
775
885
  const templateValues = {
@@ -793,6 +903,7 @@ async function buildNginxBundleFromSource(source, options) {
793
903
  appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
794
904
  indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
795
905
  indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
906
+ indexSettingsPath: resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
796
907
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
797
908
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
798
909
  appPublicPath: context.appPublicPath,
@@ -810,6 +921,7 @@ async function buildNginxBundleFromSource(source, options) {
810
921
  }),
811
922
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
812
923
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
924
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
813
925
  };
814
926
  }
815
927
  export async function buildEnvProxyCaddyBundle(runtime, options) {
@@ -826,21 +938,26 @@ async function buildCaddyBundleFromSource(source, options) {
826
938
  const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
827
939
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
828
940
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
829
- const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
941
+ const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
942
+ const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
830
943
  readFile(sourceIndexV1Path, 'utf8'),
831
944
  readFile(sourceIndexV2Path, 'utf8'),
945
+ readFile(sourceIndexSettingsPath, 'utf8'),
832
946
  ]);
833
947
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v1'));
834
948
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v2'));
949
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'settings'));
835
950
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
836
951
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
837
952
  const indexV1AssetPublicPath = context.cdnBaseUrl;
838
953
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
954
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
839
955
  const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
840
956
  const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
841
957
  const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
842
958
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
843
959
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
960
+ activeVersion: context.activeVersion,
844
961
  appPublicPath: context.appPublicPath,
845
962
  apiBasePath: context.apiBasePath,
846
963
  apiPort: context.apiPort,
@@ -861,6 +978,7 @@ async function buildCaddyBundleFromSource(source, options) {
861
978
  appConfigPath,
862
979
  indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
863
980
  indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
981
+ indexSettingsPath: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
864
982
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
865
983
  appPublicPath: context.appPublicPath,
866
984
  apiBasePath: context.apiBasePath,
@@ -874,6 +992,7 @@ async function buildCaddyBundleFromSource(source, options) {
874
992
  mainConfigContent: await buildEnvProxyMainConfig({ provider: 'caddy', scope: options?.scope }),
875
993
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
876
994
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
995
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
877
996
  };
878
997
  }
879
998
  async function pathExists(candidate) {
@@ -1155,10 +1274,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1155
1274
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1156
1275
  const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
1157
1276
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
1277
+ const settingsAssetsPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}settings/assets/`);
1278
+ const settingsAssetsRoot = joinRuntimePath(context.distClientRoot, `${context.activeVersion}/settings/assets`);
1279
+ const settingsRoutePattern = `^${escapeRegExp(context.appPublicPath)}settings(?:/.*)?$`;
1158
1280
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
1159
1281
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
1160
1282
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
1161
1283
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
1284
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
1162
1285
  const rootRedirectBlock = context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1163
1286
  ? ''
1164
1287
  : `
@@ -1229,6 +1352,12 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1229
1352
  ' file_server',
1230
1353
  ' }',
1231
1354
  '',
1355
+ ` handle_path ${settingsAssetsPathMatcher} {`,
1356
+ ` root * ${settingsAssetsRoot}`,
1357
+ ' header Cache-Control "public, max-age=31536000, immutable"',
1358
+ ' file_server',
1359
+ ' }',
1360
+ '',
1232
1361
  ' @oauth path_regexp oauth ^/\\.well-known/oauth-authorization-server/(.+)$',
1233
1362
  ' handle @oauth {',
1234
1363
  ' rewrite * /{re.oauth.1}/.well-known/oauth-authorization-server',
@@ -1262,6 +1391,23 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1262
1391
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1263
1392
  ' }',
1264
1393
  '',
1394
+ ` handle ${portalBasePath} {`,
1395
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1396
+ ' }',
1397
+ '',
1398
+ ` handle ${portalBasePath}/* {`,
1399
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1400
+ ' }',
1401
+ '',
1402
+ ` @settingsRoute path_regexp settingsRoute ${settingsRoutePattern}`,
1403
+ ' handle @settingsRoute {',
1404
+ ` root * ${publicDir}`,
1405
+ ' header Cache-Control "no-store, no-cache, must-revalidate"',
1406
+ ' header X-Robots-Tag "noindex, nofollow"',
1407
+ ' try_files {path} /index-settings.html',
1408
+ ' file_server',
1409
+ ' }',
1410
+ '',
1265
1411
  ' # Keep the v2 SPA route above the fallback SPA route.',
1266
1412
  ` handle_path ${toCaddyPathMatcher(context.v2PublicPath)} {`,
1267
1413
  ` root * ${publicDir}`,
@@ -1334,6 +1480,7 @@ async function buildEnvProxyRenderState(runtime, options) {
1334
1480
  : await mapProxyPathFromCliRoot(distClientRoot, options);
1335
1481
  const provider = resolveProxyProviderName(options?.provider);
1336
1482
  const templateContext = {
1483
+ activeVersion: runtimeVersion,
1337
1484
  appPublicPath: settings.appPublicPath,
1338
1485
  apiBasePath: settings.apiBasePath,
1339
1486
  apiPort,
@@ -6,18 +6,30 @@
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 { normalizeAppClientEntryMode } from './app-client-entry-mode.js';
12
+ import { resolveEnvKind } from './auth-store.js';
11
13
  import { resolveConfiguredEnvPath } from './cli-home.js';
12
- import { resolveDockerEnvFileArg } from "./docker-env-file.js";
14
+ import { resolveDockerEnvFileArg, resolveDockerEnvFilePath } from "./docker-env-file.js";
13
15
  import { resolveConfiguredAppPath } from './env-paths.js';
16
+ export const DEFAULT_MANAGED_ENV_FILE_VALUES = {
17
+ APP_DISCOVERY_ADAPTER: 'local',
18
+ APP_PROCESS_ADAPTER: 'local',
19
+ APP_CLIENT_ENTRY_MODE: 'modern-only',
20
+ };
21
+ function buildManagedEnvFileDefaults(config, defaults = DEFAULT_MANAGED_ENV_FILE_VALUES) {
22
+ return {
23
+ ...defaults,
24
+ APP_CLIENT_ENTRY_MODE: normalizeAppClientEntryMode(config?.appClientEntryMode) ??
25
+ trimValue(defaults.APP_CLIENT_ENTRY_MODE) ??
26
+ DEFAULT_MANAGED_ENV_FILE_VALUES.APP_CLIENT_ENTRY_MODE,
27
+ };
28
+ }
14
29
  function trimValue(value) {
15
30
  const text = String(value ?? '').trim();
16
31
  return text || undefined;
17
32
  }
18
- function normalizeEnvFilePath(value) {
19
- return value.replace(/\\/g, '/');
20
- }
21
33
  function stripWrappingQuotes(value) {
22
34
  if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
23
35
  return value
@@ -33,6 +45,29 @@ function stripWrappingQuotes(value) {
33
45
  }
34
46
  return value;
35
47
  }
48
+ function escapeRegExp(value) {
49
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ }
51
+ function upsertSimpleEnvContent(content, values) {
52
+ let nextContent = content;
53
+ const missingEntries = new Map(Object.entries(values).filter(([, value]) => trimValue(value)));
54
+ for (const key of Array.from(missingEntries.keys())) {
55
+ const pattern = new RegExp(`^\\s*(?:export\\s+)?${escapeRegExp(key)}\\s*=.*$`, 'gm');
56
+ if (!pattern.test(nextContent)) {
57
+ continue;
58
+ }
59
+ nextContent = nextContent.replace(pattern, `${key}=${values[key]}`);
60
+ missingEntries.delete(key);
61
+ }
62
+ if (missingEntries.size === 0) {
63
+ return nextContent;
64
+ }
65
+ const separator = nextContent && !nextContent.endsWith('\n') ? '\n' : '';
66
+ const appended = Array.from(missingEntries.entries())
67
+ .map(([key, value]) => `${key}=${value}`)
68
+ .join('\n');
69
+ return `${nextContent}${separator}${appended}\n`;
70
+ }
36
71
  export function parseSimpleEnvFile(content) {
37
72
  const values = {};
38
73
  for (const rawLine of content.split(/\r?\n/)) {
@@ -57,16 +92,91 @@ export function resolveManagedLocalEnvFilePath(runtime) {
57
92
  const config = runtime.env.config ?? {};
58
93
  const explicitEnvFile = trimValue(config.envFile);
59
94
  if (explicitEnvFile) {
60
- return normalizeEnvFilePath(resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile);
95
+ return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
61
96
  }
62
97
  const configuredAppPath = resolveConfiguredAppPath(config);
63
98
  if (configuredAppPath) {
64
- return normalizeEnvFilePath(path.join(configuredAppPath, '.env'));
99
+ return path.join(configuredAppPath, '.env');
65
100
  }
66
101
  if (path.basename(runtime.projectRoot) === 'source') {
67
- return normalizeEnvFilePath(path.resolve(runtime.projectRoot, '..', '.env'));
102
+ return path.resolve(runtime.projectRoot, '..', '.env');
103
+ }
104
+ return path.join(runtime.projectRoot, '.env');
105
+ }
106
+ export function resolveManagedEnvFilePathFromConfig(envName, config) {
107
+ const kind = config?.kind ?? resolveEnvKind(config);
108
+ if (kind === 'docker') {
109
+ return resolveDockerEnvFilePath(envName, config);
110
+ }
111
+ if (kind !== 'local') {
112
+ return undefined;
113
+ }
114
+ const explicitEnvFile = trimValue(config?.envFile);
115
+ if (explicitEnvFile) {
116
+ return resolveConfiguredEnvPath(explicitEnvFile) ?? explicitEnvFile;
117
+ }
118
+ const configuredAppPath = resolveConfiguredAppPath(config);
119
+ if (configuredAppPath) {
120
+ return path.join(configuredAppPath, '.env');
121
+ }
122
+ const configuredAppRootPath = trimValue(config?.appRootPath);
123
+ if (configuredAppRootPath) {
124
+ const appRootPath = resolveConfiguredEnvPath(configuredAppRootPath) ?? configuredAppRootPath;
125
+ return path.basename(appRootPath) === 'source'
126
+ ? path.resolve(appRootPath, '..', '.env')
127
+ : path.join(appRootPath, '.env');
128
+ }
129
+ return undefined;
130
+ }
131
+ export async function ensureManagedEnvFileDefaults(envName, config, defaults = DEFAULT_MANAGED_ENV_FILE_VALUES) {
132
+ const envFilePath = resolveManagedEnvFilePathFromConfig(envName, config);
133
+ if (!envFilePath) {
134
+ return undefined;
135
+ }
136
+ let content = '';
137
+ try {
138
+ content = await readFile(envFilePath, 'utf8');
139
+ }
140
+ catch (error) {
141
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
142
+ if (code !== 'ENOENT') {
143
+ throw error;
144
+ }
145
+ }
146
+ const existing = parseSimpleEnvFile(content);
147
+ const resolvedDefaults = buildManagedEnvFileDefaults(config, defaults);
148
+ const missingEntries = Object.entries(resolvedDefaults).filter(([key, value]) => trimValue(value) && !existing[key]);
149
+ if (missingEntries.length === 0) {
150
+ return envFilePath;
151
+ }
152
+ const separator = content && !content.endsWith('\n') ? '\n' : '';
153
+ const nextContent = `${content}${separator}${missingEntries.map(([key, value]) => `${key}=${value}`).join('\n')}\n`;
154
+ await mkdir(path.dirname(envFilePath), { recursive: true });
155
+ await writeFile(envFilePath, nextContent, 'utf8');
156
+ return envFilePath;
157
+ }
158
+ export async function upsertManagedEnvFileValues(envName, config, values) {
159
+ const envFilePath = resolveManagedEnvFilePathFromConfig(envName, config);
160
+ if (!envFilePath) {
161
+ return undefined;
162
+ }
163
+ let content = '';
164
+ try {
165
+ content = await readFile(envFilePath, 'utf8');
166
+ }
167
+ catch (error) {
168
+ const code = error && typeof error === 'object' && 'code' in error ? String(error.code) : '';
169
+ if (code !== 'ENOENT') {
170
+ throw error;
171
+ }
172
+ }
173
+ const nextContent = upsertSimpleEnvContent(content, values);
174
+ if (nextContent === content) {
175
+ return envFilePath;
68
176
  }
69
- return normalizeEnvFilePath(path.join(runtime.projectRoot, '.env'));
177
+ await mkdir(path.dirname(envFilePath), { recursive: true });
178
+ await writeFile(envFilePath, nextContent, 'utf8');
179
+ return envFilePath;
70
180
  }
71
181
  export async function resolveManagedRuntimeEnvFilePath(runtime) {
72
182
  if (runtime.kind === 'local') {
@@ -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,8 @@ 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_TEMPLATE', config?.portalTemplate);
33
+ }
31
34
  return out;
32
35
  }