@nocobase/cli 2.2.0-beta.8 → 2.2.0-test.15

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/assets/env-proxy/nginx/app.conf.tpl +23 -0
  2. package/assets/env-proxy/nginx/nocobase.conf.tpl +5 -0
  3. package/assets/env-proxy/nginx/snippets/dist-location.conf +5 -0
  4. package/assets/env-proxy/nginx/snippets/gzip.conf +17 -0
  5. package/assets/env-proxy/nginx/snippets/log-format-http.conf +13 -0
  6. package/assets/env-proxy/nginx/snippets/maps-http.conf +14 -0
  7. package/assets/env-proxy/nginx/snippets/mime-types.conf +98 -0
  8. package/assets/env-proxy/nginx/snippets/proxy-location.conf +18 -0
  9. package/assets/env-proxy/nginx/snippets/spa-location.conf +6 -0
  10. package/assets/env-proxy/nginx/snippets/uploads-location.conf +24 -0
  11. package/dist/commands/app/start.js +25 -2
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/init.js +131 -4
  15. package/dist/commands/install.js +179 -131
  16. package/dist/commands/portal/create.js +105 -0
  17. package/dist/commands/portal/deploy.js +81 -0
  18. package/dist/commands/portal/destroy.js +104 -0
  19. package/dist/commands/portal/dev.js +71 -0
  20. package/dist/commands/portal/index.js +20 -0
  21. package/dist/commands/portal/info.js +82 -0
  22. package/dist/commands/portal/list.js +98 -0
  23. package/dist/commands/portal/pull.js +77 -0
  24. package/dist/commands/portal/push.js +79 -0
  25. package/dist/commands/proxy/caddy/generate.js +93 -7
  26. package/dist/commands/proxy/nginx/generate.js +98 -7
  27. package/dist/commands/revision/create.js +1 -1
  28. package/dist/commands/source/dev.js +1 -1
  29. package/dist/commands/source/download.js +18 -14
  30. package/dist/lib/app-managed-resources.js +3 -2
  31. package/dist/lib/auth-store.js +71 -1
  32. package/dist/lib/cli-config.js +74 -2
  33. package/dist/lib/docker-image.js +94 -6
  34. package/dist/lib/env-config.js +8 -0
  35. package/dist/lib/env-proxy-config.js +48 -0
  36. package/dist/lib/env-proxy.js +266 -61
  37. package/dist/lib/managed-init-env.js +6 -1
  38. package/dist/lib/portal-command-env.js +31 -0
  39. package/dist/lib/portal-create.js +488 -0
  40. package/dist/lib/portal-deploy.js +275 -0
  41. package/dist/lib/portal-destroy.js +100 -0
  42. package/dist/lib/portal-dev.js +79 -0
  43. package/dist/lib/portal-env-files.js +53 -0
  44. package/dist/lib/portal-info.js +31 -0
  45. package/dist/lib/portal-list.js +197 -0
  46. package/dist/lib/portal-source.js +416 -0
  47. package/dist/lib/portal-template.js +190 -0
  48. package/dist/lib/prompt-catalog-terminal.js +32 -19
  49. package/dist/lib/prompt-web-ui.js +13 -2
  50. package/dist/lib/proxy-caddy.js +77 -9
  51. package/dist/lib/proxy-nginx.js +71 -11
  52. package/dist/lib/run-npm.js +17 -16
  53. package/dist/lib/ui.js +28 -1
  54. package/dist/locale/en-US.json +192 -38
  55. package/dist/locale/zh-CN.json +192 -38
  56. package/package.json +6 -3
@@ -23,6 +23,7 @@ const DEFAULT_MODERN_CLIENT_PREFIX = 'v';
23
23
  const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
24
24
  const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
25
25
  const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
26
+ const PORTAL_CLIENT_PREFIX = 'x';
26
27
  const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
27
28
  const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
28
29
  const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
@@ -259,7 +260,7 @@ export function applyEnvProxyAppEntryOptions(content, provider, options) {
259
260
  function toCaddyPathMatcher(prefixPath) {
260
261
  return `${prefixPath}*`;
261
262
  }
262
- export async function loadEnvProxySettings(runtime) {
263
+ export async function loadEnvProxySettings(runtime, options) {
263
264
  const { envFilePath, envValues } = await readManagedRuntimeEnvValues(runtime);
264
265
  const appPublicPath = resolveAppPublicPath(runtime.env.config.appPublicPath || envValues.APP_PUBLIC_PATH || DEFAULT_APP_PUBLIC_PATH);
265
266
  const apiClientShareToken = /^true$/i.test(String(envValues.API_CLIENT_SHARE_TOKEN ?? '').trim());
@@ -274,7 +275,9 @@ export async function loadEnvProxySettings(runtime) {
274
275
  wsPath: prefixRuntimePath(appPublicPath, envValues.WS_PATH || DEFAULT_WS_PATH),
275
276
  pluginStaticsPath: prefixRuntimePath(appPublicPath, envValues.PLUGIN_STATICS_PATH || DEFAULT_PLUGIN_STATICS_PATH, { trailingSlash: true }),
276
277
  modernClientPrefix: normalizeModernClientPrefix(envValues.APP_MODERN_CLIENT_PREFIX),
277
- cdnBaseUrl: trimValue(runtime.env.envVars?.CDN_BASE_URL) ?? trimValue(envValues.CDN_BASE_URL),
278
+ cdnBaseUrl: trimValue(options?.cdnBaseUrl) ??
279
+ trimValue(runtime.env.envVars?.CDN_BASE_URL) ??
280
+ trimValue(envValues.CDN_BASE_URL),
278
281
  apiClientStoragePrefix: trimValue(envValues.API_CLIENT_STORAGE_PREFIX) ?? DEFAULT_API_CLIENT_STORAGE_PREFIX,
279
282
  apiClientStorageType: trimValue(envValues.API_CLIENT_STORAGE_TYPE) ?? DEFAULT_API_CLIENT_STORAGE_TYPE,
280
283
  apiClientShareToken,
@@ -284,6 +287,53 @@ export async function loadEnvProxySettings(runtime) {
284
287
  },
285
288
  };
286
289
  }
290
+ function createManualProxyEnvSettings(input) {
291
+ const appPublicPath = resolveAppPublicPath(input.appPublicPath || DEFAULT_APP_PUBLIC_PATH);
292
+ return {
293
+ appPublicPath,
294
+ apiBasePath: prefixRuntimePath(appPublicPath, DEFAULT_API_BASE_PATH, {
295
+ trailingSlash: true,
296
+ }),
297
+ distPath: resolveDistPublicPath(appPublicPath),
298
+ wsPath: prefixRuntimePath(appPublicPath, DEFAULT_WS_PATH),
299
+ pluginStaticsPath: prefixRuntimePath(appPublicPath, DEFAULT_PLUGIN_STATICS_PATH, {
300
+ trailingSlash: true,
301
+ }),
302
+ modernClientPrefix: DEFAULT_MODERN_CLIENT_PREFIX,
303
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
304
+ apiClientStoragePrefix: DEFAULT_API_CLIENT_STORAGE_PREFIX,
305
+ apiClientStorageType: DEFAULT_API_CLIENT_STORAGE_TYPE,
306
+ apiClientShareToken: false,
307
+ wsUrl: '',
308
+ esmCdnBaseUrl: DEFAULT_ESM_CDN_BASE_URL,
309
+ esmCdnSuffix: '',
310
+ };
311
+ }
312
+ function normalizeManualNginxInput(input) {
313
+ const upstreamPort = trimValue(input.upstreamPort) ?? trimValue(input.appPort);
314
+ return {
315
+ name: String(input.name).trim(),
316
+ storagePath: String(input.storagePath).trim(),
317
+ distRootPath: String(input.distRootPath).trim(),
318
+ runtimeVersion: String(input.runtimeVersion).trim(),
319
+ appPublicPath: trimValue(input.appPublicPath),
320
+ upstreamHost: trimValue(input.upstreamHost),
321
+ upstreamPort,
322
+ appPort: trimValue(input.appPort),
323
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
324
+ };
325
+ }
326
+ function normalizeProxyPort(value) {
327
+ const normalized = trimValue(value);
328
+ if (!normalized || !/^\d+$/.test(normalized)) {
329
+ return undefined;
330
+ }
331
+ const port = Number(normalized);
332
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
333
+ return undefined;
334
+ }
335
+ return normalized;
336
+ }
287
337
  async function parseVersionFromPackageJson(content, sourceLabel) {
288
338
  let parsed;
289
339
  try {
@@ -438,7 +488,9 @@ function rewriteHtmlAssetPublicPath(html, currentPublicPath, nextPublicPath) {
438
488
  }
439
489
  function buildNginxManagedConfigBlock(context) {
440
490
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
491
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
441
492
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
493
+ const fileAccessPath = `${context.appPublicPath}files/`;
442
494
  const isRootMounted = context.appPublicPath === '/';
443
495
  const appPublicPathRedirectBlock = isRootMounted
444
496
  ? ''
@@ -476,6 +528,26 @@ function buildNginxManagedConfigBlock(context) {
476
528
  ` include ${context.snippetsDir}/proxy-location.conf;`,
477
529
  ' }',
478
530
  '',
531
+ ` location ^~ ${fileAccessPath} {`,
532
+ ` proxy_pass ${context.backendUrl};`,
533
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
534
+ ' }',
535
+ ...(!isRootMounted
536
+ ? [
537
+ '',
538
+ ' location ^~ /files/ {',
539
+ ` proxy_pass ${context.backendUrl};`,
540
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
541
+ ' }',
542
+ ]
543
+ : []),
544
+ '',
545
+ buildNginxPortalLocationBlock(context),
546
+ '',
547
+ ` location = ${apiBasePathNoTrailingSlash} {`,
548
+ ` return 308 ${context.apiBasePath}$is_args$args;`,
549
+ ' }',
550
+ '',
479
551
  ` location ^~ ${context.apiBasePath} {`,
480
552
  ` proxy_pass ${context.backendUrl};`,
481
553
  ` include ${context.snippetsDir}/proxy-location.conf;`,
@@ -497,7 +569,7 @@ function buildNginxManagedConfigBlock(context) {
497
569
  ` include ${context.snippetsDir}/spa-location.conf;`,
498
570
  ' }',
499
571
  '',
500
- ` location ^~ ${context.appPublicPath} {`,
572
+ ` location ${context.appPublicPath} {`,
501
573
  ` alias ${context.publicDir}/;`,
502
574
  ` try_files $uri /index-v1.html =404;`,
503
575
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -506,6 +578,65 @@ function buildNginxManagedConfigBlock(context) {
506
578
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
507
579
  ].join('\n');
508
580
  }
581
+ function buildNginxPortalRootPublicPath(appPublicPath) {
582
+ return appPublicPath === DEFAULT_APP_PUBLIC_PATH
583
+ ? `/${PORTAL_CLIENT_PREFIX}/`
584
+ : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
585
+ }
586
+ function buildNginxPortalLocationBlock(context) {
587
+ const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
588
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
589
+ return [
590
+ ` location ^~ ${portalBasePath}/apps/ {`,
591
+ ' absolute_redirect off;',
592
+ '',
593
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
594
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
595
+ ' }',
596
+ '',
597
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
598
+ ' return 404;',
599
+ ' }',
600
+ '',
601
+ ` root ${context.storageDir};`,
602
+ '',
603
+ ' if ($portal_path = "") {',
604
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
605
+ ' }',
606
+ '',
607
+ ' try_files',
608
+ ' /portals/$subapp/$portal/dist/$portal_path',
609
+ ' /portals/$subapp/$portal/dist/$portal_path/',
610
+ ' /portals/$subapp/$portal/dist/index.html',
611
+ ' =404;',
612
+ ' }',
613
+ '',
614
+ ` location ^~ ${portalBasePath}/ {`,
615
+ ' absolute_redirect off;',
616
+ '',
617
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
618
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
619
+ ' }',
620
+ '',
621
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
622
+ ' return 404;',
623
+ ' }',
624
+ '',
625
+ ` root ${context.storageDir};`,
626
+ '',
627
+ ' if ($portal_path = "") {',
628
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
629
+ ' }',
630
+ '',
631
+ ' try_files',
632
+ ' /portals/main/$portal/dist/$portal_path',
633
+ ' /portals/main/$portal/dist/$portal_path/',
634
+ ' /portals/main/$portal/dist/index.html',
635
+ ' =404;',
636
+ ' }',
637
+ '',
638
+ ].join('\n');
639
+ }
509
640
  function buildNginxRuntimeConfig(context, variant) {
510
641
  return {
511
642
  __webpack_public_path__: context.cdnBaseUrl,
@@ -525,65 +656,94 @@ function buildNginxRuntimeConfig(context, variant) {
525
656
  function buildCaddyRuntimeConfig(context, variant) {
526
657
  return buildNginxRuntimeConfig(context, variant);
527
658
  }
528
- async function buildEnvProxyNginxRenderContext(runtime, options) {
529
- const apiPort = trimValue(runtime.env.appPort ?? runtime.env.config.appPort);
530
- if (!apiPort) {
531
- throw new Error(translateCli('commands.envProxy.errors.missingAppPort', { envName: runtime.envName }, {
532
- fallback: `Missing appPort for env "${runtime.envName}". Save or update the app port before generating proxy config.`,
533
- }));
534
- }
535
- const activeVersion = trimValue(await readDistClientActiveVersion(runtime.env.storagePath));
536
- if (!activeVersion) {
537
- throw new Error(translateCli('commands.envProxy.errors.missingVersion', { envName: runtime.envName }, {
538
- fallback: `Couldn't determine the app version for env "${runtime.envName}". Run \`nb env update ${runtime.envName}\` and try again.`,
539
- }));
540
- }
541
- const { envFilePath, settings } = await loadEnvProxySettings(runtime);
659
+ async function buildEnvProxyNginxRenderContext(source, options) {
542
660
  const proxyHost = await resolveProxyUpstreamHost(options);
543
- const backendUrl = `http://${proxyHost}:${apiPort}`;
544
- const cdnBaseUrl = settings.cdnBaseUrl ?? buildDefaultCdnBaseUrl(settings.appPublicPath, activeVersion);
545
- const entryDir = resolveEnvProxyEntryDir(runtime.envName, { scope: options?.scope });
546
- const publicDir = resolveEnvProxyNginxPublicOutputDir(runtime.envName, { scope: options?.scope });
661
+ const upstreamPort = normalizeProxyPort(options?.upstreamPort) ?? source.apiPort;
662
+ const backendUrl = `http://${proxyHost}:${upstreamPort}`;
663
+ const cdnBaseUrl = source.settings.cdnBaseUrl ?? buildDefaultCdnBaseUrl(source.settings.appPublicPath, source.activeVersion);
664
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope });
665
+ const publicDir = resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope });
547
666
  const snippetsDir = resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope });
548
- const distRootDir = resolveDistClientRoot(runtime.env.storagePath);
549
- const uploadsDir = path.join(runtime.env.storagePath, 'uploads');
667
+ const distRootDir = source.distRootPath;
668
+ const uploadsDir = path.join(source.storagePath, 'uploads');
550
669
  const mappedEntryDir = await mapProxyPathFromCliRoot(entryDir, options);
551
670
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
552
671
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
553
672
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
673
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
554
674
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
555
- const v2PublicPath = `${settings.appPublicPath.replace(/\/$/, '')}/${settings.modernClientPrefix}/`;
675
+ const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
556
676
  return {
557
- envName: runtime.envName,
558
- envFilePath,
559
- apiBasePath: settings.apiBasePath,
560
- apiClientShareToken: settings.apiClientShareToken,
561
- apiClientStoragePrefix: settings.apiClientStoragePrefix,
562
- apiClientStorageType: settings.apiClientStorageType,
563
- apiPort,
564
- appPublicPath: settings.appPublicPath,
677
+ envName: source.envName,
678
+ envFilePath: source.envFilePath,
679
+ apiBasePath: source.settings.apiBasePath,
680
+ apiClientShareToken: source.settings.apiClientShareToken,
681
+ apiClientStoragePrefix: source.settings.apiClientStoragePrefix,
682
+ apiClientStorageType: source.settings.apiClientStorageType,
683
+ apiPort: source.apiPort,
684
+ appPublicPath: source.settings.appPublicPath,
565
685
  backendUrl,
566
686
  cdnBaseUrl: ensureTrailingSlash(cdnBaseUrl),
567
- distPath: settings.distPath,
687
+ distPath: source.settings.distPath,
568
688
  distRootDir: mappedDistRootDir,
569
689
  entryDir: mappedEntryDir,
570
690
  publicDir: mappedPublicDir,
571
- esmCdnBaseUrl: settings.esmCdnBaseUrl,
572
- esmCdnSuffix: settings.esmCdnSuffix,
573
- indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(runtime.envName, 'v1', { scope: options?.scope }), options),
574
- indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(runtime.envName, 'v2', { scope: options?.scope }), options),
575
- modernClientPrefix: settings.modernClientPrefix,
691
+ esmCdnBaseUrl: source.settings.esmCdnBaseUrl,
692
+ esmCdnSuffix: source.settings.esmCdnSuffix,
693
+ indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
694
+ indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
695
+ modernClientPrefix: source.settings.modernClientPrefix,
576
696
  proxyHost,
577
697
  snippetsDir: mappedSnippetsDir,
698
+ storageDir: mappedStorageDir,
578
699
  uploadsDir: mappedUploadsDir,
579
700
  v2PublicPath,
580
- wsPath: settings.wsPath,
581
- wsUrl: settings.wsUrl,
701
+ wsPath: source.settings.wsPath,
702
+ wsUrl: source.settings.wsUrl,
703
+ activeVersion: source.activeVersion,
704
+ };
705
+ }
706
+ async function resolveRuntimeNginxBundleSource(runtime, options) {
707
+ const apiPort = trimValue(runtime.env.appPort ?? runtime.env.config.appPort);
708
+ if (!apiPort) {
709
+ throw new Error(translateCli('commands.envProxy.errors.missingAppPort', { envName: runtime.envName }, {
710
+ fallback: `Missing appPort for env "${runtime.envName}". Save or update the app port before generating proxy config.`,
711
+ }));
712
+ }
713
+ const activeVersion = trimValue(await readDistClientActiveVersion(runtime.env.storagePath));
714
+ if (!activeVersion) {
715
+ throw new Error(translateCli('commands.envProxy.errors.missingVersion', { envName: runtime.envName }, {
716
+ fallback: `Couldn't determine the app version for env "${runtime.envName}". Run \`nb env update ${runtime.envName}\` and try again.`,
717
+ }));
718
+ }
719
+ const { envFilePath, settings } = await loadEnvProxySettings(runtime, options);
720
+ return {
721
+ envName: runtime.envName,
722
+ envFilePath,
723
+ storagePath: runtime.env.storagePath,
724
+ distRootPath: resolveDistClientRoot(runtime.env.storagePath),
725
+ settings,
726
+ apiPort,
582
727
  activeVersion,
583
728
  };
584
729
  }
730
+ async function resolveManualNginxBundleSource(input) {
731
+ const normalized = normalizeManualNginxInput(input);
732
+ return {
733
+ envName: normalized.name,
734
+ envFilePath: undefined,
735
+ storagePath: normalized.storagePath,
736
+ distRootPath: normalized.distRootPath,
737
+ settings: createManualProxyEnvSettings(normalized),
738
+ apiPort: normalized.upstreamPort,
739
+ activeVersion: normalized.runtimeVersion,
740
+ };
741
+ }
585
742
  async function buildEnvProxyCaddyRenderContext(runtime, options) {
586
- return await buildEnvProxyNginxRenderContext(runtime, {
743
+ return await buildEnvProxyCaddyRenderContextFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
744
+ }
745
+ async function buildEnvProxyCaddyRenderContextFromSource(source, options) {
746
+ return await buildEnvProxyNginxRenderContext(source, {
587
747
  ...options,
588
748
  provider: 'caddy',
589
749
  });
@@ -636,11 +796,21 @@ export function resolveEnvProxyCaddyIndexOutputPath(envName, variant, options) {
636
796
  return path.join(resolveEnvProxyCaddyPublicOutputDir(envName, { scope: options?.scope }), `index-${variant}.html`);
637
797
  }
638
798
  export async function buildEnvProxyNginxBundle(runtime, options) {
639
- const context = await buildEnvProxyNginxRenderContext(runtime, options);
799
+ return await buildNginxBundleFromSource(await resolveRuntimeNginxBundleSource(runtime, options), options);
800
+ }
801
+ export async function buildManualEnvProxyNginxBundle(input, options) {
802
+ return await buildNginxBundleFromSource(await resolveManualNginxBundleSource(input), {
803
+ ...options,
804
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
805
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
806
+ });
807
+ }
808
+ async function buildNginxBundleFromSource(source, options) {
809
+ const context = await buildEnvProxyNginxRenderContext(source, options);
640
810
  const appTemplate = await readEnvProxyNginxAssetText('app.conf.tpl');
641
811
  const mainTemplate = await readEnvProxyNginxAssetText('nocobase.conf.tpl');
642
- const sourceIndexV1Path = path.join(runtime.env.storagePath, 'dist-client', context.activeVersion, 'index.html');
643
- const sourceIndexV2Path = path.join(runtime.env.storagePath, 'dist-client', context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
812
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
813
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
644
814
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
645
815
  readFile(sourceIndexV1Path, 'utf8'),
646
816
  readFile(sourceIndexV2Path, 'utf8'),
@@ -667,13 +837,13 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
667
837
  wsPath: context.wsPath,
668
838
  };
669
839
  return {
670
- envName: runtime.envName,
840
+ envName: source.envName,
671
841
  envFilePath: context.envFilePath,
672
- entryDir: resolveEnvProxyEntryDir(runtime.envName, { scope: options?.scope, provider: 'nginx' }),
673
- publicDir: resolveEnvProxyNginxPublicOutputDir(runtime.envName, { scope: options?.scope }),
674
- appConfigPath: resolveEnvProxyAppOutputPath(runtime.envName, { scope: options?.scope, provider: 'nginx' }),
675
- indexV1Path: resolveEnvProxyNginxIndexOutputPath(runtime.envName, 'v1', { scope: options?.scope }),
676
- indexV2Path: resolveEnvProxyNginxIndexOutputPath(runtime.envName, 'v2', { scope: options?.scope }),
842
+ entryDir: resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'nginx' }),
843
+ publicDir: resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope }),
844
+ appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
845
+ indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
846
+ indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
677
847
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
678
848
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
679
849
  appPublicPath: context.appPublicPath,
@@ -694,9 +864,19 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
694
864
  };
695
865
  }
696
866
  export async function buildEnvProxyCaddyBundle(runtime, options) {
697
- const context = await buildEnvProxyCaddyRenderContext(runtime, options);
698
- const sourceIndexV1Path = path.join(runtime.env.storagePath, 'dist-client', context.activeVersion, 'index.html');
699
- const sourceIndexV2Path = path.join(runtime.env.storagePath, 'dist-client', context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
867
+ return await buildCaddyBundleFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
868
+ }
869
+ export async function buildManualEnvProxyCaddyBundle(input, options) {
870
+ return await buildCaddyBundleFromSource(await resolveManualNginxBundleSource(input), {
871
+ ...options,
872
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
873
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
874
+ });
875
+ }
876
+ async function buildCaddyBundleFromSource(source, options) {
877
+ const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
878
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
879
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
700
880
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
701
881
  readFile(sourceIndexV1Path, 'utf8'),
702
882
  readFile(sourceIndexV2Path, 'utf8'),
@@ -707,9 +887,9 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
707
887
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
708
888
  const indexV1AssetPublicPath = context.cdnBaseUrl;
709
889
  const indexV2AssetPublicPath = `${trimTrailingSlash(context.cdnBaseUrl)}/${DEFAULT_MODERN_CLIENT_PREFIX}/`;
710
- const appConfigPath = resolveEnvProxyAppOutputPath(runtime.envName, { scope: options?.scope, provider: 'caddy' });
711
- const entryDir = resolveEnvProxyEntryDir(runtime.envName, { scope: options?.scope, provider: 'caddy' });
712
- const publicDir = resolveEnvProxyCaddyPublicOutputDir(runtime.envName, { scope: options?.scope });
890
+ const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
891
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
892
+ const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
713
893
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
714
894
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
715
895
  appPublicPath: context.appPublicPath,
@@ -725,13 +905,13 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
725
905
  wsPath: context.wsPath,
726
906
  }, renderedPublicDir);
727
907
  return {
728
- envName: runtime.envName,
908
+ envName: source.envName,
729
909
  envFilePath: context.envFilePath,
730
910
  entryDir,
731
911
  publicDir,
732
912
  appConfigPath,
733
- indexV1Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v1', { scope: options?.scope }),
734
- indexV2Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v2', { scope: options?.scope }),
913
+ indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
914
+ indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
735
915
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
736
916
  appPublicPath: context.appPublicPath,
737
917
  apiBasePath: context.apiBasePath,
@@ -893,20 +1073,23 @@ function buildNginxOtherLocation(appPublicPath, v2PublicPath, modernClientPrefix
893
1073
  function renderNginxLocationTemplate(context) {
894
1074
  const proxyPassBlock = buildNginxProxyPassBlock(context.proxyHost, context.apiPort);
895
1075
  const wsProxyPassTarget = `http://${context.proxyHost}:${context.apiPort}${context.wsPath}`;
1076
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
896
1077
  return ` location ~* ^${context.appPublicPath}storage/uploads/(.*\\.md)$ {
897
1078
  alias ${context.uploadsPath}/$1;
898
1079
  default_type text/markdown;
899
1080
  add_header Cache-Control "public";
900
1081
  add_header Content-Disposition "inline";
1082
+ add_header Content-Security-Policy "sandbox" always;
901
1083
  add_header X-Content-Type-Options "nosniff" always;
902
1084
  access_log off;
903
1085
  autoindex off;
904
1086
  }
905
1087
 
906
- location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|svg|svgz|xhtml|pdf))$ {
1088
+ location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
907
1089
  alias ${context.uploadsPath}/$1;
908
1090
  add_header Cache-Control "public";
909
1091
  add_header Content-Disposition "attachment" always;
1092
+ add_header Content-Security-Policy "sandbox" always;
910
1093
  add_header X-Content-Type-Options "nosniff" always;
911
1094
  access_log off;
912
1095
  autoindex off;
@@ -915,6 +1098,7 @@ function renderNginxLocationTemplate(context) {
915
1098
  location ${context.appPublicPath}storage/uploads/ {
916
1099
  alias ${context.uploadsPath}/;
917
1100
  add_header Cache-Control "public";
1101
+ add_header Content-Security-Policy "sandbox" always;
918
1102
  add_header X-Content-Type-Options "nosniff" always;
919
1103
  access_log off;
920
1104
  autoindex off;
@@ -938,6 +1122,10 @@ function renderNginxLocationTemplate(context) {
938
1122
  ${proxyPassBlock}
939
1123
  }${context.otherLocation}
940
1124
 
1125
+ location = ${apiBasePathNoTrailingSlash} {
1126
+ return 308 ${context.apiBasePath}$is_args$args;
1127
+ }
1128
+
941
1129
  location ^~ ${context.apiBasePath} {
942
1130
  ${proxyPassBlock}
943
1131
  }
@@ -989,6 +1177,7 @@ function buildCaddyContextCommentLines(siteAddress, context, publicDir) {
989
1177
  }
990
1178
  function renderCaddyAppTemplate(siteAddress, context, publicDir) {
991
1179
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1180
+ const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
992
1181
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
993
1182
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
994
1183
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
@@ -1030,10 +1219,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1030
1219
  `${siteAddress} {`,
1031
1220
  ` encode zstd gzip${rootRedirectBlock}${appPublicPathRedirectBlock}${modernClientRedirectBlock}${shorthandModernClientRedirectBlock}`,
1032
1221
  '',
1222
+ ' @activeUploadedContent path_regexp activeUploadedContent (?i)\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$',
1223
+ '',
1033
1224
  ` handle_path ${uploadsPathMatcher} {`,
1034
1225
  ` root * ${context.uploadsPath}`,
1035
1226
  ' header Cache-Control public',
1227
+ ' header Content-Security-Policy sandbox',
1036
1228
  ' header X-Content-Type-Options nosniff',
1229
+ ' header @activeUploadedContent Content-Disposition attachment',
1037
1230
  ' file_server',
1038
1231
  ' }',
1039
1232
  '',
@@ -1055,7 +1248,19 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1055
1248
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1056
1249
  ' }',
1057
1250
  '',
1058
- ' # Keep API and WS routes above the SPA fallbacks.',
1251
+ ' # Keep file, API and WS routes above the SPA fallbacks.',
1252
+ ` handle ${fileAccessPathMatcher} {`,
1253
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1254
+ ' }',
1255
+ ...(context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1256
+ ? []
1257
+ : [
1258
+ '',
1259
+ ' handle /files/* {',
1260
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1261
+ ' }',
1262
+ ]),
1263
+ '',
1059
1264
  ` handle ${apiPathMatcher} {`,
1060
1265
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1061
1266
  ' }',
@@ -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_DEVELOPMENT_MODE', config?.developmentMode);
33
+ put('INIT_PORTAL_NAME', config?.portalName);
34
+ put('INIT_PORTAL_TEMPLATE', config?.portalTemplate);
35
+ }
31
36
  return out;
32
37
  }
@@ -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
+ }