@nocobase/cli 2.3.0-alpha.1 → 3.0.0-alpha.2

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 (43) hide show
  1. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  2. package/dist/commands/config/set.js +1 -0
  3. package/dist/commands/init.js +13 -5
  4. package/dist/commands/install.js +104 -3
  5. package/dist/commands/portal/config.js +88 -0
  6. package/dist/commands/portal/create.js +105 -0
  7. package/dist/commands/portal/deploy.js +81 -0
  8. package/dist/commands/portal/destroy.js +104 -0
  9. package/dist/commands/portal/dev.js +71 -0
  10. package/dist/commands/portal/index.js +20 -0
  11. package/dist/commands/portal/info.js +82 -0
  12. package/dist/commands/portal/list.js +98 -0
  13. package/dist/commands/portal/pull.js +84 -0
  14. package/dist/commands/portal/push.js +79 -0
  15. package/dist/commands/source/dev.js +1 -1
  16. package/dist/lib/api-client.js +7 -0
  17. package/dist/lib/auth-store.js +3 -1
  18. package/dist/lib/cli-config.js +20 -1
  19. package/dist/lib/env-auth.js +2 -2
  20. package/dist/lib/env-config.js +3 -0
  21. package/dist/lib/env-proxy.js +141 -8
  22. package/dist/lib/managed-env-file.js +58 -2
  23. package/dist/lib/managed-init-env.js +6 -1
  24. package/dist/lib/naming.js +9 -0
  25. package/dist/lib/portal-command-env.js +31 -0
  26. package/dist/lib/portal-config.js +133 -0
  27. package/dist/lib/portal-configure.js +117 -0
  28. package/dist/lib/portal-create.js +433 -0
  29. package/dist/lib/portal-deploy.js +298 -0
  30. package/dist/lib/portal-destroy.js +100 -0
  31. package/dist/lib/portal-dev.js +79 -0
  32. package/dist/lib/portal-env-files.js +53 -0
  33. package/dist/lib/portal-info.js +31 -0
  34. package/dist/lib/portal-list.js +211 -0
  35. package/dist/lib/portal-source.js +523 -0
  36. package/dist/lib/proxy-caddy.js +2 -0
  37. package/dist/lib/proxy-nginx.js +1 -0
  38. package/dist/lib/run-npm.js +17 -16
  39. package/dist/lib/ui.js +28 -1
  40. package/dist/locale/en-US.json +178 -0
  41. package/dist/locale/zh-CN.json +178 -0
  42. package/nocobase-ctl.config.json +111 -0
  43. package/package.json +5 -2
@@ -36,6 +36,9 @@ const STRING_ENV_CONFIG_KEYS = [
36
36
  'dbSchema',
37
37
  'dbTablePrefix',
38
38
  'lang',
39
+ 'portalType',
40
+ 'portalName',
41
+ 'portalTemplate',
39
42
  'rootUsername',
40
43
  'rootEmail',
41
44
  'rootPassword',
@@ -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']);
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,71 @@ function buildNginxManagedConfigBlock(context) {
575
605
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
576
606
  ].join('\n');
577
607
  }
608
+ function buildNginxPortalRootPublicPath(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(buildNginxPortalRootPublicPath(context.appPublicPath));
615
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
616
+ return [
617
+ ` location ^~ ${portalBasePath}/apps/ {`,
618
+ ' absolute_redirect off;',
619
+ '',
620
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
621
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
622
+ ' }',
623
+ '',
624
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
625
+ ' return 404;',
626
+ ' }',
627
+ '',
628
+ ` root ${context.storageDir};`,
629
+ '',
630
+ ' if ($portal_path = "") {',
631
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
632
+ ' }',
633
+ '',
634
+ ' try_files',
635
+ ' /portals/$subapp/$portal/dist/$portal_path',
636
+ ' /portals/$subapp/$portal/dist/$portal_path/',
637
+ ' /portals/$subapp/$portal/dist/index.html',
638
+ ' =404;',
639
+ ' }',
640
+ '',
641
+ ` location ^~ ${portalBasePath}/ {`,
642
+ ' absolute_redirect off;',
643
+ '',
644
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
645
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
646
+ ' }',
647
+ '',
648
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
649
+ ' return 404;',
650
+ ' }',
651
+ '',
652
+ ` root ${context.storageDir};`,
653
+ '',
654
+ ' if ($portal_path = "") {',
655
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
656
+ ' }',
657
+ '',
658
+ ' try_files',
659
+ ' /portals/main/$portal/dist/$portal_path',
660
+ ' /portals/main/$portal/dist/$portal_path/',
661
+ ' /portals/main/$portal/dist/index.html',
662
+ ' =404;',
663
+ ' }',
664
+ '',
665
+ ].join('\n');
666
+ }
578
667
  function buildNginxRuntimeConfig(context, variant) {
579
668
  return {
580
669
  __webpack_public_path__: context.cdnBaseUrl,
581
- __nocobase_public_path__: variant === 'v1' ? context.appPublicPath : context.v2PublicPath,
582
- ...(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,
583
673
  __nocobase_api_base_url__: context.apiBasePath,
584
674
  __nocobase_api_client_storage_prefix__: context.apiClientStoragePrefix,
585
675
  __nocobase_api_client_storage_type__: context.apiClientStorageType,
@@ -608,6 +698,7 @@ async function buildEnvProxyNginxRenderContext(source, options) {
608
698
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
609
699
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
610
700
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
701
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
611
702
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
612
703
  const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
613
704
  return {
@@ -629,9 +720,12 @@ async function buildEnvProxyNginxRenderContext(source, options) {
629
720
  esmCdnSuffix: source.settings.esmCdnSuffix,
630
721
  indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
631
722
  indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
723
+ indexSettingsPath: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }), options),
632
724
  modernClientPrefix: source.settings.modernClientPrefix,
725
+ appClientEntryMode: source.settings.appClientEntryMode,
633
726
  proxyHost,
634
727
  snippetsDir: mappedSnippetsDir,
728
+ storageDir: mappedStorageDir,
635
729
  uploadsDir: mappedUploadsDir,
636
730
  v2PublicPath,
637
731
  wsPath: source.settings.wsPath,
@@ -747,16 +841,20 @@ async function buildNginxBundleFromSource(source, options) {
747
841
  const mainTemplate = await readEnvProxyNginxAssetText('nocobase.conf.tpl');
748
842
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
749
843
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
750
- 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([
751
846
  readFile(sourceIndexV1Path, 'utf8'),
752
847
  readFile(sourceIndexV2Path, 'utf8'),
848
+ readFile(sourceIndexSettingsPath, 'utf8'),
753
849
  ]);
754
850
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v1'));
755
851
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'v2'));
852
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildNginxRuntimeConfig(context, 'settings'));
756
853
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
757
854
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
758
855
  const indexV1AssetPublicPath = context.cdnBaseUrl;
759
856
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
857
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
760
858
  const appConfigIncludePath = await mapProxyPathFromCliRoot(path.join(resolveEnvProxyProviderRootDir('nginx', { scope: options?.scope }), '*', resolveEnvProxyFileSpec('nginx').appFilename), options);
761
859
  const managedConfigBlock = buildNginxManagedConfigBlock(context);
762
860
  const templateValues = {
@@ -780,6 +878,7 @@ async function buildNginxBundleFromSource(source, options) {
780
878
  appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
781
879
  indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
782
880
  indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
881
+ indexSettingsPath: resolveEnvProxyNginxIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
783
882
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
784
883
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
785
884
  appPublicPath: context.appPublicPath,
@@ -797,6 +896,7 @@ async function buildNginxBundleFromSource(source, options) {
797
896
  }),
798
897
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
799
898
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
899
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
800
900
  };
801
901
  }
802
902
  export async function buildEnvProxyCaddyBundle(runtime, options) {
@@ -813,21 +913,26 @@ async function buildCaddyBundleFromSource(source, options) {
813
913
  const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
814
914
  const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
815
915
  const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
816
- 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([
817
918
  readFile(sourceIndexV1Path, 'utf8'),
818
919
  readFile(sourceIndexV2Path, 'utf8'),
920
+ readFile(sourceIndexSettingsPath, 'utf8'),
819
921
  ]);
820
922
  const v1RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v1'));
821
923
  const v2RuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'v2'));
924
+ const settingsRuntimeScript = buildRuntimeConfigScriptTag(buildCaddyRuntimeConfig(context, 'settings'));
822
925
  const sourceV1PublicPath = extractRuntimePublicPath(sourceIndexV1Content);
823
926
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
824
927
  const indexV1AssetPublicPath = context.cdnBaseUrl;
825
928
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
929
+ const indexSettingsAssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/settings/`;
826
930
  const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
827
931
  const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
828
932
  const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
829
933
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
830
934
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
935
+ activeVersion: context.activeVersion,
831
936
  appPublicPath: context.appPublicPath,
832
937
  apiBasePath: context.apiBasePath,
833
938
  apiPort: context.apiPort,
@@ -848,6 +953,7 @@ async function buildCaddyBundleFromSource(source, options) {
848
953
  appConfigPath,
849
954
  indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
850
955
  indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
956
+ indexSettingsPath: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'settings', { scope: options?.scope }),
851
957
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
852
958
  appPublicPath: context.appPublicPath,
853
959
  apiBasePath: context.apiBasePath,
@@ -861,6 +967,7 @@ async function buildCaddyBundleFromSource(source, options) {
861
967
  mainConfigContent: await buildEnvProxyMainConfig({ provider: 'caddy', scope: options?.scope }),
862
968
  indexV1Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV1Content, sourceV1PublicPath, indexV1AssetPublicPath), v1RuntimeScript),
863
969
  indexV2Content: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexV2Content, sourceV2PublicPath, indexV2AssetPublicPath), v2RuntimeScript),
970
+ indexSettingsContent: injectRuntimeScriptIntoHtml(rewriteHtmlAssetPublicPath(sourceIndexSettingsContent, '/settings/', indexSettingsAssetPublicPath), settingsRuntimeScript),
864
971
  };
865
972
  }
866
973
  async function pathExists(candidate) {
@@ -1015,15 +1122,17 @@ function renderNginxLocationTemplate(context) {
1015
1122
  default_type text/markdown;
1016
1123
  add_header Cache-Control "public";
1017
1124
  add_header Content-Disposition "inline";
1125
+ add_header Content-Security-Policy "sandbox" always;
1018
1126
  add_header X-Content-Type-Options "nosniff" always;
1019
1127
  access_log off;
1020
1128
  autoindex off;
1021
1129
  }
1022
1130
 
1023
- location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|svg|svgz|xhtml|pdf))$ {
1131
+ location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
1024
1132
  alias ${context.uploadsPath}/$1;
1025
1133
  add_header Cache-Control "public";
1026
1134
  add_header Content-Disposition "attachment" always;
1135
+ add_header Content-Security-Policy "sandbox" always;
1027
1136
  add_header X-Content-Type-Options "nosniff" always;
1028
1137
  access_log off;
1029
1138
  autoindex off;
@@ -1032,6 +1141,7 @@ function renderNginxLocationTemplate(context) {
1032
1141
  location ${context.appPublicPath}storage/uploads/ {
1033
1142
  alias ${context.uploadsPath}/;
1034
1143
  add_header Cache-Control "public";
1144
+ add_header Content-Security-Policy "sandbox" always;
1035
1145
  add_header X-Content-Type-Options "nosniff" always;
1036
1146
  access_log off;
1037
1147
  autoindex off;
@@ -1112,6 +1222,9 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1112
1222
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1113
1223
  const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
1114
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(?:/.*)?$`;
1115
1228
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
1116
1229
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
1117
1230
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
@@ -1152,10 +1265,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1152
1265
  `${siteAddress} {`,
1153
1266
  ` encode zstd gzip${rootRedirectBlock}${appPublicPathRedirectBlock}${modernClientRedirectBlock}${shorthandModernClientRedirectBlock}`,
1154
1267
  '',
1268
+ ' @activeUploadedContent path_regexp activeUploadedContent (?i)\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$',
1269
+ '',
1155
1270
  ` handle_path ${uploadsPathMatcher} {`,
1156
1271
  ` root * ${context.uploadsPath}`,
1157
1272
  ' header Cache-Control public',
1273
+ ' header Content-Security-Policy sandbox',
1158
1274
  ' header X-Content-Type-Options nosniff',
1275
+ ' header @activeUploadedContent Content-Disposition attachment',
1159
1276
  ' file_server',
1160
1277
  ' }',
1161
1278
  '',
@@ -1165,6 +1282,12 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1165
1282
  ' file_server',
1166
1283
  ' }',
1167
1284
  '',
1285
+ ` handle_path ${settingsAssetsPathMatcher} {`,
1286
+ ` root * ${settingsAssetsRoot}`,
1287
+ ' header Cache-Control "public, max-age=31536000, immutable"',
1288
+ ' file_server',
1289
+ ' }',
1290
+ '',
1168
1291
  ' @oauth path_regexp oauth ^/\\.well-known/oauth-authorization-server/(.+)$',
1169
1292
  ' handle @oauth {',
1170
1293
  ' rewrite * /{re.oauth.1}/.well-known/oauth-authorization-server',
@@ -1198,6 +1321,15 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1198
1321
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1199
1322
  ' }',
1200
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
+ '',
1201
1333
  ' # Keep the v2 SPA route above the fallback SPA route.',
1202
1334
  ` handle_path ${toCaddyPathMatcher(context.v2PublicPath)} {`,
1203
1335
  ` root * ${publicDir}`,
@@ -1270,6 +1402,7 @@ async function buildEnvProxyRenderState(runtime, options) {
1270
1402
  : await mapProxyPathFromCliRoot(distClientRoot, options);
1271
1403
  const provider = resolveProxyProviderName(options?.provider);
1272
1404
  const templateContext = {
1405
+ activeVersion: runtimeVersion,
1273
1406
  appPublicPath: settings.appPublicPath,
1274
1407
  apiBasePath: settings.apiBasePath,
1275
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);
@@ -15,7 +15,7 @@ export function resolveManagedSetupState(value) {
15
15
  export function isPreparedSetupState(value) {
16
16
  return resolveManagedSetupState(value) === 'prepared';
17
17
  }
18
- export function buildInitAppEnvVarsFromConfig(config) {
18
+ export function buildInitAppEnvVarsFromConfig(config, options = {}) {
19
19
  const out = {};
20
20
  const put = (key, value) => {
21
21
  const text = trimValue(value);
@@ -28,5 +28,10 @@ export function buildInitAppEnvVarsFromConfig(config) {
28
28
  put('INIT_ROOT_EMAIL', config?.rootEmail);
29
29
  put('INIT_ROOT_PASSWORD', config?.rootPassword);
30
30
  put('INIT_ROOT_NICKNAME', config?.rootNickname);
31
+ if (options.includePortal !== false) {
32
+ put('INIT_PORTAL_TYPE', config?.portalType);
33
+ put('INIT_PORTAL_NAME', config?.portalName);
34
+ put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
35
+ }
31
36
  return out;
32
37
  }
@@ -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,31 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ const PORTAL_COMMAND_BASE_ENV_KEYS = [
10
+ 'PATH',
11
+ 'Path',
12
+ 'PATHEXT',
13
+ 'SystemRoot',
14
+ 'WINDIR',
15
+ 'ComSpec',
16
+ 'HOME',
17
+ 'USERPROFILE',
18
+ 'TMPDIR',
19
+ 'TEMP',
20
+ 'TMP',
21
+ ];
22
+ export function buildPortalCommandEnv(env = {}) {
23
+ const out = {};
24
+ for (const key of PORTAL_COMMAND_BASE_ENV_KEYS) {
25
+ const value = process.env[key];
26
+ if (value) {
27
+ out[key] = value;
28
+ }
29
+ }
30
+ return { ...out, ...env };
31
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+ import { readFile, writeFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { executeApiRequest } from './api-client.js';
12
+ import { translateCli } from './cli-locale.js';
13
+ export const DEFAULT_PORTAL_GIT_PATH = '.';
14
+ const portalConfigText = (key, values, fallback) => translateCli(`commands.portalConfig.${key}`, values, { fallback });
15
+ const UPDATE_PORTAL_OPERATION = {
16
+ method: 'POST',
17
+ pathTemplate: '/multiPortals:update',
18
+ hasBody: true,
19
+ bodyRequired: true,
20
+ parameters: [
21
+ {
22
+ name: 'filterByTk',
23
+ flagName: 'filterByTk',
24
+ in: 'query',
25
+ required: true,
26
+ },
27
+ ],
28
+ };
29
+ function trimValue(value) {
30
+ return String(value ?? '').trim();
31
+ }
32
+ function readObject(value) {
33
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
34
+ }
35
+ function validatePortalSourceStorage(value) {
36
+ const sourceStorage = trimValue(value) || 'nocobase';
37
+ if (sourceStorage === 'nocobase' || sourceStorage === 'git') {
38
+ return sourceStorage;
39
+ }
40
+ throw new Error(portalConfigText('errors.invalidSourceStorage', { value: sourceStorage }, `Invalid source storage "${sourceStorage}". Use "nocobase" or "git".`));
41
+ }
42
+ function isFullGitRemoteUrl(value) {
43
+ return /^(?:https?:\/\/|ssh:\/\/|file:\/\/|git@[^:]+:).+/.test(value);
44
+ }
45
+ function validateGitPath(value) {
46
+ const gitPath = trimValue(value);
47
+ if (!gitPath || path.isAbsolute(gitPath) || gitPath.split(/[\\/]+/).includes('..')) {
48
+ throw new Error(portalConfigText('errors.invalidGitPath', { value }, '--git-path must be a relative path inside the Git repository.'));
49
+ }
50
+ return gitPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
51
+ }
52
+ export function buildPortalConfig(options) {
53
+ const sourceStorage = validatePortalSourceStorage(options.sourceStorage ?? options.existingConfig?.sourceStorage);
54
+ const hasGitOption = Boolean(trimValue(options.gitRepo) || trimValue(options.gitBranch) || trimValue(options.gitPath));
55
+ if (sourceStorage === 'nocobase') {
56
+ if (hasGitOption) {
57
+ throw new Error(portalConfigText('errors.gitOptionsForNocobaseStorage', undefined, '--git-repo, --git-branch, and --git-path can only be used with --source-storage git.'));
58
+ }
59
+ return { sourceStorage };
60
+ }
61
+ const repo = trimValue(options.gitRepo) || options.existingConfig?.git?.repo || '';
62
+ if (!repo) {
63
+ throw new Error(portalConfigText('errors.gitRepoRequired', undefined, '--git-repo is required when --source-storage is git.'));
64
+ }
65
+ if (!isFullGitRemoteUrl(repo)) {
66
+ throw new Error(portalConfigText('errors.gitRepoInvalid', undefined, '--git-repo must be a full Git remote URL.'));
67
+ }
68
+ return {
69
+ sourceStorage,
70
+ git: {
71
+ repo,
72
+ branch: trimValue(options.gitBranch) || options.existingConfig?.git?.branch || 'main',
73
+ path: validateGitPath(trimValue(options.gitPath) || options.existingConfig?.git?.path || DEFAULT_PORTAL_GIT_PATH),
74
+ },
75
+ };
76
+ }
77
+ export function buildPortalConfigFromOptions(options, portal) {
78
+ const sourceOptions = readObject(options);
79
+ const git = readObject(sourceOptions.git);
80
+ return buildPortalConfig({
81
+ portal,
82
+ sourceStorage: trimValue(sourceOptions.sourceStorage) || 'nocobase',
83
+ gitRepo: trimValue(git.repo),
84
+ gitBranch: trimValue(git.branch),
85
+ gitPath: trimValue(git.path),
86
+ });
87
+ }
88
+ export function mergePortalConfigIntoOptions(config, currentOptions) {
89
+ const nextOptions = {
90
+ ...(currentOptions ?? {}),
91
+ sourceStorage: config.sourceStorage,
92
+ };
93
+ if (config.sourceStorage === 'git') {
94
+ nextOptions.git = config.git;
95
+ }
96
+ else {
97
+ delete nextOptions.git;
98
+ }
99
+ return nextOptions;
100
+ }
101
+ export async function readPortalConfig(portalDir) {
102
+ const configPath = path.join(portalDir, 'portal.config.json');
103
+ const data = JSON.parse(await readFile(configPath, 'utf-8'));
104
+ const config = readObject(data);
105
+ const git = readObject(config.git);
106
+ return buildPortalConfig({
107
+ portal: path.basename(portalDir),
108
+ sourceStorage: trimValue(config.sourceStorage),
109
+ gitRepo: trimValue(git.repo),
110
+ gitBranch: trimValue(git.branch),
111
+ gitPath: trimValue(git.path),
112
+ });
113
+ }
114
+ export async function writePortalConfig(portalDir, config) {
115
+ await writeFile(path.join(portalDir, 'portal.config.json'), `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
116
+ }
117
+ export async function syncPortalConfigToRemote(options) {
118
+ const apiRequest = options.apiRequest ?? executeApiRequest;
119
+ const response = await apiRequest({
120
+ cliVersion: options.cliVersion ?? '',
121
+ envName: options.envName,
122
+ flags: {
123
+ filterByTk: options.portal,
124
+ body: JSON.stringify({
125
+ options: mergePortalConfigIntoOptions(options.config, options.currentOptions),
126
+ }),
127
+ },
128
+ operation: UPDATE_PORTAL_OPERATION,
129
+ });
130
+ if (!response.ok) {
131
+ throw new Error(portalConfigText('errors.updateFailed', { status: response.status, details: JSON.stringify(response.data, null, 2) }, `Portal config update failed with status ${response.status}\n${JSON.stringify(response.data, null, 2)}`));
132
+ }
133
+ }