@nocobase/cli 2.3.0-beta.6 → 2.4.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 (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
  ? ''
@@ -541,6 +558,8 @@ function buildNginxManagedConfigBlock(context) {
541
558
  ]
542
559
  : []),
543
560
  '',
561
+ buildNginxPortalLocationBlock(context),
562
+ '',
544
563
  ` location = ${apiBasePathNoTrailingSlash} {`,
545
564
  ` return 308 ${context.apiBasePath}$is_args$args;`,
546
565
  ' }',
@@ -560,13 +579,24 @@ function buildNginxManagedConfigBlock(context) {
560
579
  ` return 302 ${context.v2PublicPath}$is_args$args;`,
561
580
  ' }',
562
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
+ '',
563
593
  ` location ^~ ${context.v2PublicPath} {`,
564
594
  ` alias ${context.publicDir}/;`,
565
595
  ` try_files $uri /index-v2.html =404;`,
566
596
  ` include ${context.snippetsDir}/spa-location.conf;`,
567
597
  ' }',
568
598
  '',
569
- ` location ^~ ${context.appPublicPath} {`,
599
+ ` location ${context.appPublicPath} {`,
570
600
  ` alias ${context.publicDir}/;`,
571
601
  ` try_files $uri /index-v1.html =404;`,
572
602
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -575,11 +605,83 @@ function buildNginxManagedConfigBlock(context) {
575
605
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
576
606
  ].join('\n');
577
607
  }
608
+ function buildPortalRootPublicPath(appPublicPath) {
609
+ return appPublicPath === DEFAULT_APP_PUBLIC_PATH
610
+ ? `/${PORTAL_CLIENT_PREFIX}/`
611
+ : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
612
+ }
613
+ function buildNginxPortalLocationBlock(context) {
614
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
615
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
616
+ return [
617
+ ` location = ${portalBasePath} {`,
618
+ ' absolute_redirect off;',
619
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
620
+ ' }',
621
+ '',
622
+ ` location = ${portalBasePath}/ {`,
623
+ ' absolute_redirect off;',
624
+ ` return 302 ${context.v2PublicPath}$is_args$args;`,
625
+ ' }',
626
+ '',
627
+ ` location ^~ ${portalBasePath}/apps/ {`,
628
+ ' absolute_redirect off;',
629
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/?$) {`,
630
+ ` return 302 ${context.v2PublicPath}apps/$subapp/$is_args$args;`,
631
+ ' }',
632
+ '',
633
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
634
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
635
+ ' }',
636
+ '',
637
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
638
+ ' return 404;',
639
+ ' }',
640
+ '',
641
+ ` root ${context.storageDir};`,
642
+ '',
643
+ ' if ($portal_path = "") {',
644
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
645
+ ' }',
646
+ '',
647
+ ' try_files',
648
+ ' /portals/$subapp/$portal/dist/$portal_path',
649
+ ' /portals/$subapp/$portal/dist/$portal_path/',
650
+ ' /portals/$subapp/$portal/dist/index.html',
651
+ ' =404;',
652
+ ' }',
653
+ '',
654
+ ` location ^~ ${portalBasePath}/ {`,
655
+ ' absolute_redirect off;',
656
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
657
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
658
+ ' }',
659
+ '',
660
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
661
+ ' return 404;',
662
+ ' }',
663
+ '',
664
+ ` root ${context.storageDir};`,
665
+ '',
666
+ ' if ($portal_path = "") {',
667
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
668
+ ' }',
669
+ '',
670
+ ' try_files',
671
+ ' /portals/main/$portal/dist/$portal_path',
672
+ ' /portals/main/$portal/dist/$portal_path/',
673
+ ' /portals/main/$portal/dist/index.html',
674
+ ' =404;',
675
+ ' }',
676
+ '',
677
+ ].join('\n');
678
+ }
578
679
  function buildNginxRuntimeConfig(context, variant) {
579
680
  return {
580
681
  __webpack_public_path__: context.cdnBaseUrl,
581
- __nocobase_public_path__: variant === 'v1' ? context.appPublicPath : context.v2PublicPath,
582
- ...(variant === 'v2' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
682
+ __nocobase_public_path__: variant === 'v2' ? context.v2PublicPath : context.appPublicPath,
683
+ ...(variant !== 'v1' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
684
+ __nocobase_app_client_entry_mode__: context.appClientEntryMode,
583
685
  __nocobase_api_base_url__: context.apiBasePath,
584
686
  __nocobase_api_client_storage_prefix__: context.apiClientStoragePrefix,
585
687
  __nocobase_api_client_storage_type__: context.apiClientStorageType,
@@ -608,6 +710,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
608
710
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
609
711
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
610
712
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
713
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
611
714
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
612
715
  const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
613
716
  return {
@@ -629,9 +732,12 @@ async function buildEnvProxyNginxRenderContext(source, options) {
629
732
  esmCdnSuffix: source.settings.esmCdnSuffix,
630
733
  indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
631
734
  indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
735
+ indexSettingsPath: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }), options),
632
736
  modernClientPrefix: source.settings.modernClientPrefix,
737
+ appClientEntryMode: source.settings.appClientEntryMode,
633
738
  proxyHost,
634
739
  snippetsDir: mappedSnippetsDir,
740
+ storageDir: mappedStorageDir,
635
741
  uploadsDir: mappedUploadsDir,
636
742
  v2PublicPath,
637
743
  wsPath: source.settings.wsPath,
@@ -747,16 +853,20 @@ async function buildNginxBundleFromSource(source, options) {
747
853
  const mainTemplate = await readEnvProxyNginxAssetText('nocobase.conf.tpl');
748
854
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
749
855
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
750
- const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
856
+ const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
857
+ const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
751
858
  readFile(sourceIndexV1Path, 'utf8'),
752
859
  readFile(sourceIndexV2Path, 'utf8'),
860
+ readFile(sourceIndexSettingsPath, 'utf8'),
753
861
  ]);
754
862
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v1'));
755
863
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v2'));
864
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'settings'));
756
865
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
757
866
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
758
867
  const indexV1AssetPublicPath = context.cdnBaseUrl;
759
868
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
869
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
760
870
  const appConfigIncludePath = await mapProxyPathFromCliRoot(path.join(resolveEnvProxyProviderRootDir('nginx', { scope: options?.scope }), '*', resolveEnvProxyFileSpec('nginx').appFilename), options);
761
871
  const managedConfigBlock = buildNginxManagedConfigBlock(context);
762
872
  const templateValues = {
@@ -780,6 +890,7 @@ async function buildNginxBundleFromSource(source, options) {
780
890
  appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
781
891
  indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
782
892
  indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
893
+ indexSettingsPath: resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
783
894
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
784
895
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
785
896
  appPublicPath: context.appPublicPath,
@@ -797,6 +908,7 @@ async function buildNginxBundleFromSource(source, options) {
797
908
  }),
798
909
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
799
910
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
911
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
800
912
  };
801
913
  }
802
914
  export async function buildEnvProxyCaddyBundle(runtime, options) {
@@ -813,21 +925,26 @@ async function buildCaddyBundleFromSource(source, options) {
813
925
  const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
814
926
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
815
927
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
816
- const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
928
+ const sourceIndexSettingsPath = path.join(source.distRootPath, context.activeVersion, 'settings', 'index.html');
929
+ const [sourceIndexV1Content, sourceIndexV2Content, sourceIndexSettingsContent] = await Promise.all([
817
930
  readFile(sourceIndexV1Path, 'utf8'),
818
931
  readFile(sourceIndexV2Path, 'utf8'),
932
+ readFile(sourceIndexSettingsPath, 'utf8'),
819
933
  ]);
820
934
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v1'));
821
935
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v2'));
936
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'settings'));
822
937
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
823
938
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
824
939
  const indexV1AssetPublicPath = context.cdnBaseUrl;
825
940
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
941
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
826
942
  const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
827
943
  const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
828
944
  const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
829
945
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
830
946
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
947
+ activeVersion: context.activeVersion,
831
948
  appPublicPath: context.appPublicPath,
832
949
  apiBasePath: context.apiBasePath,
833
950
  apiPort: context.apiPort,
@@ -848,6 +965,7 @@ async function buildCaddyBundleFromSource(source, options) {
848
965
  appConfigPath,
849
966
  indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
850
967
  indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
968
+ indexSettingsPath: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
851
969
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
852
970
  appPublicPath: context.appPublicPath,
853
971
  apiBasePath: context.apiBasePath,
@@ -861,6 +979,7 @@ async function buildCaddyBundleFromSource(source, options) {
861
979
  mainConfigContent: await buildEnvProxyMainConfig({ provider: 'caddy', scope: options?.scope }),
862
980
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
863
981
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
982
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
864
983
  };
865
984
  }
866
985
  async function pathExists(candidate) {
@@ -1115,10 +1234,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1115
1234
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1116
1235
  const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
1117
1236
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
1237
+ const settingsAssetsPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}settings/assets/`);
1238
+ const settingsAssetsRoot = joinRuntimePath(context.distClientRoot, `${context.activeVersion}/settings/assets`);
1239
+ const settingsRoutePattern = `^${escapeRegExp(context.appPublicPath)}settings(?:/.*)?$`;
1118
1240
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
1119
1241
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
1120
1242
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
1121
1243
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
1244
+ const portalBasePath = trimTrailingSlash(buildPortalRootPublicPath(context.appPublicPath));
1122
1245
  const rootRedirectBlock = context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1123
1246
  ? ''
1124
1247
  : `
@@ -1172,6 +1295,12 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1172
1295
  ' file_server',
1173
1296
  ' }',
1174
1297
  '',
1298
+ ` handle_path ${settingsAssetsPathMatcher} {`,
1299
+ ` root * ${settingsAssetsRoot}`,
1300
+ ' header Cache-Control "public, max-age=31536000, immutable"',
1301
+ ' file_server',
1302
+ ' }',
1303
+ '',
1175
1304
  ' @oauth path_regexp oauth ^/\\.well-known/oauth-authorization-server/(.+)$',
1176
1305
  ' handle @oauth {',
1177
1306
  ' rewrite * /{re.oauth.1}/.well-known/oauth-authorization-server',
@@ -1205,6 +1334,23 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1205
1334
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1206
1335
  ' }',
1207
1336
  '',
1337
+ ` handle ${portalBasePath} {`,
1338
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1339
+ ' }',
1340
+ '',
1341
+ ` handle ${portalBasePath}/* {`,
1342
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1343
+ ' }',
1344
+ '',
1345
+ ` @settingsRoute path_regexp settingsRoute ${settingsRoutePattern}`,
1346
+ ' handle @settingsRoute {',
1347
+ ` root * ${publicDir}`,
1348
+ ' header Cache-Control "no-store, no-cache, must-revalidate"',
1349
+ ' header X-Robots-Tag "noindex, nofollow"',
1350
+ ' try_files {path} /index-settings.html',
1351
+ ' file_server',
1352
+ ' }',
1353
+ '',
1208
1354
  ' # Keep the v2 SPA route above the fallback SPA route.',
1209
1355
  ` handle_path ${toCaddyPathMatcher(context.v2PublicPath)} {`,
1210
1356
  ` root * ${publicDir}`,
@@ -1277,6 +1423,7 @@ async function buildEnvProxyRenderState(runtime, options) {
1277
1423
  : await mapProxyPathFromCliRoot(distClientRoot, options);
1278
1424
  const provider = resolveProxyProviderName(options?.provider);
1279
1425
  const templateContext = {
1426
+ activeVersion: runtimeVersion,
1280
1427
  appPublicPath: settings.appPublicPath,
1281
1428
  apiBasePath: settings.apiBasePath,
1282
1429
  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
  }