@nocobase/cli 2.2.0-alpha.1 → 2.2.0-alpha.11

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 (78) hide show
  1. package/assets/env-proxy/nginx/snippets/proxy-location.conf +1 -0
  2. package/assets/env-proxy/nginx/snippets/uploads-location.conf +4 -1
  3. package/bin/early-locale.js +89 -0
  4. package/bin/node-version.js +35 -0
  5. package/bin/run.js +9 -0
  6. package/bin/windows-admin.js +60 -0
  7. package/dist/commands/app/destroy.js +4 -3
  8. package/dist/commands/app/restart.js +38 -0
  9. package/dist/commands/app/shared.js +49 -3
  10. package/dist/commands/app/start.js +95 -0
  11. package/dist/commands/app/upgrade.js +11 -0
  12. package/dist/commands/config/set.js +1 -0
  13. package/dist/commands/env/info.js +11 -1
  14. package/dist/commands/examples/prompts-stages.js +2 -2
  15. package/dist/commands/examples/prompts-test.js +2 -2
  16. package/dist/commands/init.js +152 -14
  17. package/dist/commands/install.js +256 -109
  18. package/dist/commands/license/activate.js +4 -1
  19. package/dist/commands/license/shared.js +24 -15
  20. package/dist/commands/portal/config.js +88 -0
  21. package/dist/commands/portal/create.js +104 -0
  22. package/dist/commands/portal/deploy.js +81 -0
  23. package/dist/commands/portal/destroy.js +104 -0
  24. package/dist/commands/portal/dev.js +71 -0
  25. package/dist/commands/portal/index.js +20 -0
  26. package/dist/commands/portal/info.js +82 -0
  27. package/dist/commands/portal/list.js +98 -0
  28. package/dist/commands/portal/pull.js +84 -0
  29. package/dist/commands/portal/push.js +79 -0
  30. package/dist/commands/proxy/caddy/generate.js +93 -7
  31. package/dist/commands/proxy/nginx/generate.js +98 -7
  32. package/dist/commands/revision/create.js +1 -1
  33. package/dist/commands/self/check.js +1 -1
  34. package/dist/commands/self/update.js +4 -4
  35. package/dist/commands/skills/check.js +4 -5
  36. package/dist/commands/skills/install.js +18 -1
  37. package/dist/commands/skills/update.js +19 -4
  38. package/dist/commands/source/dev.js +10 -6
  39. package/dist/commands/source/download.js +85 -16
  40. package/dist/lib/api-command-compat.js +51 -8
  41. package/dist/lib/app-managed-resources.js +104 -5
  42. package/dist/lib/auth-store.js +105 -13
  43. package/dist/lib/cli-config.js +93 -2
  44. package/dist/lib/docker-image.js +94 -6
  45. package/dist/lib/env-auth.js +291 -45
  46. package/dist/lib/env-config.js +14 -0
  47. package/dist/lib/env-proxy-config.js +48 -0
  48. package/dist/lib/env-proxy.js +276 -61
  49. package/dist/lib/hook-script.js +160 -0
  50. package/dist/lib/managed-init-env.js +6 -1
  51. package/dist/lib/portal-command-env.js +31 -0
  52. package/dist/lib/portal-config.js +133 -0
  53. package/dist/lib/portal-configure.js +117 -0
  54. package/dist/lib/portal-create.js +433 -0
  55. package/dist/lib/portal-deploy.js +283 -0
  56. package/dist/lib/portal-destroy.js +100 -0
  57. package/dist/lib/portal-dev.js +79 -0
  58. package/dist/lib/portal-env-files.js +53 -0
  59. package/dist/lib/portal-info.js +31 -0
  60. package/dist/lib/portal-list.js +211 -0
  61. package/dist/lib/portal-source.js +523 -0
  62. package/dist/lib/prompt-catalog-terminal.js +32 -19
  63. package/dist/lib/prompt-validators.js +1 -1
  64. package/dist/lib/prompt-web-ui.js +20 -13
  65. package/dist/lib/proxy-caddy.js +77 -9
  66. package/dist/lib/proxy-nginx.js +71 -11
  67. package/dist/lib/run-npm.js +21 -16
  68. package/dist/lib/self-manager.js +254 -46
  69. package/dist/lib/skills-manager.js +116 -23
  70. package/dist/lib/source-publish.js +2 -2
  71. package/dist/lib/startup-update.js +1 -1
  72. package/dist/lib/ui.js +28 -1
  73. package/dist/locale/en-US.json +227 -43
  74. package/dist/locale/zh-CN.json +227 -43
  75. package/package.json +11 -2
  76. package/scripts/build.mjs +0 -34
  77. package/scripts/clean.mjs +0 -9
  78. package/tsconfig.json +0 -19
@@ -20,9 +20,12 @@ 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 DEFAULT_APP_CLIENT_ENTRY_MODE = 'legacy-default';
24
+ const APP_CLIENT_ENTRY_MODES = new Set(['legacy-default', 'modern-default', 'modern-only']);
23
25
  const DEFAULT_API_CLIENT_STORAGE_PREFIX = 'NOCOBASE_';
24
26
  const DEFAULT_API_CLIENT_STORAGE_TYPE = 'localStorage';
25
27
  const DEFAULT_ESM_CDN_BASE_URL = 'https://esm.sh';
28
+ const PORTAL_CLIENT_PREFIX = 'x';
26
29
  const LOCAL_APP_PACKAGE_JSON_PATH = 'node_modules/@nocobase/app/package.json';
27
30
  const MANAGED_PROXY_BLOCK_BEGIN = '# BEGIN NocoBase proxy';
28
31
  const MANAGED_PROXY_BLOCK_END = '# END NocoBase proxy';
@@ -68,6 +71,10 @@ function normalizeModernClientPrefix(value) {
68
71
  .replace(/^\/+|\/+$/g, '');
69
72
  return segment || DEFAULT_MODERN_CLIENT_PREFIX;
70
73
  }
74
+ function normalizeAppClientEntryMode(value) {
75
+ const normalized = String(value || '').trim();
76
+ return APP_CLIENT_ENTRY_MODES.has(normalized) ? normalized : DEFAULT_APP_CLIENT_ENTRY_MODE;
77
+ }
71
78
  function normalizeApiBasePath(value = DEFAULT_API_BASE_PATH) {
72
79
  return resolveAppPublicPath(value);
73
80
  }
@@ -259,7 +266,7 @@ export function applyEnvProxyAppEntryOptions(content, provider, options) {
259
266
  function toCaddyPathMatcher(prefixPath) {
260
267
  return `${prefixPath}*`;
261
268
  }
262
- export async function loadEnvProxySettings(runtime) {
269
+ export async function loadEnvProxySettings(runtime, options) {
263
270
  const { envFilePath, envValues } = await readManagedRuntimeEnvValues(runtime);
264
271
  const appPublicPath = resolveAppPublicPath(runtime.env.config.appPublicPath || envValues.APP_PUBLIC_PATH || DEFAULT_APP_PUBLIC_PATH);
265
272
  const apiClientShareToken = /^true$/i.test(String(envValues.API_CLIENT_SHARE_TOKEN ?? '').trim());
@@ -274,7 +281,10 @@ export async function loadEnvProxySettings(runtime) {
274
281
  wsPath: prefixRuntimePath(appPublicPath, envValues.WS_PATH || DEFAULT_WS_PATH),
275
282
  pluginStaticsPath: prefixRuntimePath(appPublicPath, envValues.PLUGIN_STATICS_PATH || DEFAULT_PLUGIN_STATICS_PATH, { trailingSlash: true }),
276
283
  modernClientPrefix: normalizeModernClientPrefix(envValues.APP_MODERN_CLIENT_PREFIX),
277
- cdnBaseUrl: trimValue(runtime.env.envVars?.CDN_BASE_URL) ?? trimValue(envValues.CDN_BASE_URL),
284
+ appClientEntryMode: normalizeAppClientEntryMode(envValues.APP_CLIENT_ENTRY_MODE),
285
+ cdnBaseUrl: trimValue(options?.cdnBaseUrl) ??
286
+ trimValue(runtime.env.envVars?.CDN_BASE_URL) ??
287
+ trimValue(envValues.CDN_BASE_URL),
278
288
  apiClientStoragePrefix: trimValue(envValues.API_CLIENT_STORAGE_PREFIX) ?? DEFAULT_API_CLIENT_STORAGE_PREFIX,
279
289
  apiClientStorageType: trimValue(envValues.API_CLIENT_STORAGE_TYPE) ?? DEFAULT_API_CLIENT_STORAGE_TYPE,
280
290
  apiClientShareToken,
@@ -284,6 +294,54 @@ export async function loadEnvProxySettings(runtime) {
284
294
  },
285
295
  };
286
296
  }
297
+ function createManualProxyEnvSettings(input) {
298
+ const appPublicPath = resolveAppPublicPath(input.appPublicPath || DEFAULT_APP_PUBLIC_PATH);
299
+ return {
300
+ appPublicPath,
301
+ apiBasePath: prefixRuntimePath(appPublicPath, DEFAULT_API_BASE_PATH, {
302
+ trailingSlash: true,
303
+ }),
304
+ distPath: resolveDistPublicPath(appPublicPath),
305
+ wsPath: prefixRuntimePath(appPublicPath, DEFAULT_WS_PATH),
306
+ pluginStaticsPath: prefixRuntimePath(appPublicPath, DEFAULT_PLUGIN_STATICS_PATH, {
307
+ trailingSlash: true,
308
+ }),
309
+ modernClientPrefix: DEFAULT_MODERN_CLIENT_PREFIX,
310
+ appClientEntryMode: normalizeAppClientEntryMode(process.env.APP_CLIENT_ENTRY_MODE),
311
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
312
+ apiClientStoragePrefix: DEFAULT_API_CLIENT_STORAGE_PREFIX,
313
+ apiClientStorageType: DEFAULT_API_CLIENT_STORAGE_TYPE,
314
+ apiClientShareToken: false,
315
+ wsUrl: '',
316
+ esmCdnBaseUrl: DEFAULT_ESM_CDN_BASE_URL,
317
+ esmCdnSuffix: '',
318
+ };
319
+ }
320
+ function normalizeManualNginxInput(input) {
321
+ const upstreamPort = trimValue(input.upstreamPort) ?? trimValue(input.appPort);
322
+ return {
323
+ name: String(input.name).trim(),
324
+ storagePath: String(input.storagePath).trim(),
325
+ distRootPath: String(input.distRootPath).trim(),
326
+ runtimeVersion: String(input.runtimeVersion).trim(),
327
+ appPublicPath: trimValue(input.appPublicPath),
328
+ upstreamHost: trimValue(input.upstreamHost),
329
+ upstreamPort,
330
+ appPort: trimValue(input.appPort),
331
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
332
+ };
333
+ }
334
+ function normalizeProxyPort(value) {
335
+ const normalized = trimValue(value);
336
+ if (!normalized || !/^\d+$/.test(normalized)) {
337
+ return undefined;
338
+ }
339
+ const port = Number(normalized);
340
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
341
+ return undefined;
342
+ }
343
+ return normalized;
344
+ }
287
345
  async function parseVersionFromPackageJson(content, sourceLabel) {
288
346
  let parsed;
289
347
  try {
@@ -438,7 +496,9 @@ function rewriteHtmlAssetPublicPath(html, currentPublicPath, nextPublicPath) {
438
496
  }
439
497
  function buildNginxManagedConfigBlock(context) {
440
498
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
499
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
441
500
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
501
+ const fileAccessPath = `${context.appPublicPath}files/`;
442
502
  const isRootMounted = context.appPublicPath === '/';
443
503
  const appPublicPathRedirectBlock = isRootMounted
444
504
  ? ''
@@ -476,6 +536,26 @@ function buildNginxManagedConfigBlock(context) {
476
536
  ` include ${context.snippetsDir}/proxy-location.conf;`,
477
537
  ' }',
478
538
  '',
539
+ ` location ^~ ${fileAccessPath} {`,
540
+ ` proxy_pass ${context.backendUrl};`,
541
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
542
+ ' }',
543
+ ...(!isRootMounted
544
+ ? [
545
+ '',
546
+ ' location ^~ /files/ {',
547
+ ` proxy_pass ${context.backendUrl};`,
548
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
549
+ ' }',
550
+ ]
551
+ : []),
552
+ '',
553
+ buildNginxPortalLocationBlock(context),
554
+ '',
555
+ ` location = ${apiBasePathNoTrailingSlash} {`,
556
+ ` return 308 ${context.apiBasePath}$is_args$args;`,
557
+ ' }',
558
+ '',
479
559
  ` location ^~ ${context.apiBasePath} {`,
480
560
  ` proxy_pass ${context.backendUrl};`,
481
561
  ` include ${context.snippetsDir}/proxy-location.conf;`,
@@ -497,7 +577,7 @@ function buildNginxManagedConfigBlock(context) {
497
577
  ` include ${context.snippetsDir}/spa-location.conf;`,
498
578
  ' }',
499
579
  '',
500
- ` location ^~ ${context.appPublicPath} {`,
580
+ ` location ${context.appPublicPath} {`,
501
581
  ` alias ${context.publicDir}/;`,
502
582
  ` try_files $uri /index-v1.html =404;`,
503
583
  ` include ${context.snippetsDir}/spa-location.conf;`,
@@ -506,11 +586,71 @@ function buildNginxManagedConfigBlock(context) {
506
586
  ` ${MANAGED_NGINX_CONFIG_BLOCK_END}`,
507
587
  ].join('\n');
508
588
  }
589
+ function buildNginxPortalRootPublicPath(appPublicPath) {
590
+ return appPublicPath === DEFAULT_APP_PUBLIC_PATH
591
+ ? `/${PORTAL_CLIENT_PREFIX}/`
592
+ : `${trimTrailingSlash(appPublicPath)}/${PORTAL_CLIENT_PREFIX}/`;
593
+ }
594
+ function buildNginxPortalLocationBlock(context) {
595
+ const portalBasePath = trimTrailingSlash(buildNginxPortalRootPublicPath(context.appPublicPath));
596
+ const portalBasePathPattern = escapeRegExp(portalBasePath);
597
+ return [
598
+ ` location ^~ ${portalBasePath}/apps/ {`,
599
+ ' absolute_redirect off;',
600
+ '',
601
+ ` if ($uri ~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)$) {`,
602
+ ` return 308 ${portalBasePath}/apps/$subapp/$portal/$is_args$args;`,
603
+ ' }',
604
+ '',
605
+ ` if ($uri !~ ^${portalBasePathPattern}/apps/(?<subapp>[A-Za-z0-9_-]+)/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
606
+ ' return 404;',
607
+ ' }',
608
+ '',
609
+ ` root ${context.storageDir};`,
610
+ '',
611
+ ' if ($portal_path = "") {',
612
+ ' rewrite ^ /portals/$subapp/$portal/dist/index.html break;',
613
+ ' }',
614
+ '',
615
+ ' try_files',
616
+ ' /portals/$subapp/$portal/dist/$portal_path',
617
+ ' /portals/$subapp/$portal/dist/$portal_path/',
618
+ ' /portals/$subapp/$portal/dist/index.html',
619
+ ' =404;',
620
+ ' }',
621
+ '',
622
+ ` location ^~ ${portalBasePath}/ {`,
623
+ ' absolute_redirect off;',
624
+ '',
625
+ ` if ($uri ~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)$) {`,
626
+ ` return 308 ${portalBasePath}/$portal/$is_args$args;`,
627
+ ' }',
628
+ '',
629
+ ` if ($uri !~ ^${portalBasePathPattern}/(?<portal>[A-Za-z0-9_-]+)/(?<portal_path>.*)$) {`,
630
+ ' return 404;',
631
+ ' }',
632
+ '',
633
+ ` root ${context.storageDir};`,
634
+ '',
635
+ ' if ($portal_path = "") {',
636
+ ' rewrite ^ /portals/main/$portal/dist/index.html break;',
637
+ ' }',
638
+ '',
639
+ ' try_files',
640
+ ' /portals/main/$portal/dist/$portal_path',
641
+ ' /portals/main/$portal/dist/$portal_path/',
642
+ ' /portals/main/$portal/dist/index.html',
643
+ ' =404;',
644
+ ' }',
645
+ '',
646
+ ].join('\n');
647
+ }
509
648
  function buildNginxRuntimeConfig(context, variant) {
510
649
  return {
511
650
  __webpack_public_path__: context.cdnBaseUrl,
512
651
  __nocobase_public_path__: variant === 'v1' ? context.appPublicPath : context.v2PublicPath,
513
652
  ...(variant === 'v2' ? { __nocobase_modern_client_prefix__: context.modernClientPrefix } : {}),
653
+ __nocobase_app_client_entry_mode__: context.appClientEntryMode,
514
654
  __nocobase_api_base_url__: context.apiBasePath,
515
655
  __nocobase_api_client_storage_prefix__: context.apiClientStoragePrefix,
516
656
  __nocobase_api_client_storage_type__: context.apiClientStorageType,
@@ -525,65 +665,95 @@ function buildNginxRuntimeConfig(context, variant) {
525
665
  function buildCaddyRuntimeConfig(context, variant) {
526
666
  return buildNginxRuntimeConfig(context, variant);
527
667
  }
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);
668
+ async function buildEnvProxyNginxRenderContext(source, options) {
542
669
  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 });
670
+ const upstreamPort = normalizeProxyPort(options?.upstreamPort) ?? source.apiPort;
671
+ const backendUrl = `http://${proxyHost}:${upstreamPort}`;
672
+ const cdnBaseUrl = source.settings.cdnBaseUrl ?? buildDefaultCdnBaseUrl(source.settings.appPublicPath, source.activeVersion);
673
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope });
674
+ const publicDir = resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope });
547
675
  const snippetsDir = resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope });
548
- const distRootDir = resolveDistClientRoot(runtime.env.storagePath);
549
- const uploadsDir = path.join(runtime.env.storagePath, 'uploads');
676
+ const distRootDir = source.distRootPath;
677
+ const uploadsDir = path.join(source.storagePath, 'uploads');
550
678
  const mappedEntryDir = await mapProxyPathFromCliRoot(entryDir, options);
551
679
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
552
680
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
553
681
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
682
+ const mappedStorageDir = await mapProxyPathFromCliRoot(source.storagePath, options);
554
683
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
555
- const v2PublicPath = `${settings.appPublicPath.replace(/\/$/, '')}/${settings.modernClientPrefix}/`;
684
+ const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
556
685
  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,
686
+ envName: source.envName,
687
+ envFilePath: source.envFilePath,
688
+ apiBasePath: source.settings.apiBasePath,
689
+ apiClientShareToken: source.settings.apiClientShareToken,
690
+ apiClientStoragePrefix: source.settings.apiClientStoragePrefix,
691
+ apiClientStorageType: source.settings.apiClientStorageType,
692
+ apiPort: source.apiPort,
693
+ appPublicPath: source.settings.appPublicPath,
565
694
  backendUrl,
566
695
  cdnBaseUrl: ensureTrailingSlash(cdnBaseUrl),
567
- distPath: settings.distPath,
696
+ distPath: source.settings.distPath,
568
697
  distRootDir: mappedDistRootDir,
569
698
  entryDir: mappedEntryDir,
570
699
  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,
700
+ esmCdnBaseUrl: source.settings.esmCdnBaseUrl,
701
+ esmCdnSuffix: source.settings.esmCdnSuffix,
702
+ indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
703
+ indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
704
+ modernClientPrefix: source.settings.modernClientPrefix,
705
+ appClientEntryMode: source.settings.appClientEntryMode,
576
706
  proxyHost,
577
707
  snippetsDir: mappedSnippetsDir,
708
+ storageDir: mappedStorageDir,
578
709
  uploadsDir: mappedUploadsDir,
579
710
  v2PublicPath,
580
- wsPath: settings.wsPath,
581
- wsUrl: settings.wsUrl,
711
+ wsPath: source.settings.wsPath,
712
+ wsUrl: source.settings.wsUrl,
713
+ activeVersion: source.activeVersion,
714
+ };
715
+ }
716
+ async function resolveRuntimeNginxBundleSource(runtime, options) {
717
+ const apiPort = trimValue(runtime.env.appPort ?? runtime.env.config.appPort);
718
+ if (!apiPort) {
719
+ throw new Error(translateCli('commands.envProxy.errors.missingAppPort', { envName: runtime.envName }, {
720
+ fallback: `Missing appPort for env "${runtime.envName}". Save or update the app port before generating proxy config.`,
721
+ }));
722
+ }
723
+ const activeVersion = trimValue(await readDistClientActiveVersion(runtime.env.storagePath));
724
+ if (!activeVersion) {
725
+ throw new Error(translateCli('commands.envProxy.errors.missingVersion', { envName: runtime.envName }, {
726
+ fallback: `Couldn't determine the app version for env "${runtime.envName}". Run \`nb env update ${runtime.envName}\` and try again.`,
727
+ }));
728
+ }
729
+ const { envFilePath, settings } = await loadEnvProxySettings(runtime, options);
730
+ return {
731
+ envName: runtime.envName,
732
+ envFilePath,
733
+ storagePath: runtime.env.storagePath,
734
+ distRootPath: resolveDistClientRoot(runtime.env.storagePath),
735
+ settings,
736
+ apiPort,
582
737
  activeVersion,
583
738
  };
584
739
  }
740
+ async function resolveManualNginxBundleSource(input) {
741
+ const normalized = normalizeManualNginxInput(input);
742
+ return {
743
+ envName: normalized.name,
744
+ envFilePath: undefined,
745
+ storagePath: normalized.storagePath,
746
+ distRootPath: normalized.distRootPath,
747
+ settings: createManualProxyEnvSettings(normalized),
748
+ apiPort: normalized.upstreamPort,
749
+ activeVersion: normalized.runtimeVersion,
750
+ };
751
+ }
585
752
  async function buildEnvProxyCaddyRenderContext(runtime, options) {
586
- return await buildEnvProxyNginxRenderContext(runtime, {
753
+ return await buildEnvProxyCaddyRenderContextFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
754
+ }
755
+ async function buildEnvProxyCaddyRenderContextFromSource(source, options) {
756
+ return await buildEnvProxyNginxRenderContext(source, {
587
757
  ...options,
588
758
  provider: 'caddy',
589
759
  });
@@ -636,11 +806,21 @@ export function resolveEnvProxyCaddyIndexOutputPath(envName, variant, options) {
636
806
  return path.join(resolveEnvProxyCaddyPublicOutputDir(envName, { scope: options?.scope }), `index-${variant}.html`);
637
807
  }
638
808
  export async function buildEnvProxyNginxBundle(runtime, options) {
639
- const context = await buildEnvProxyNginxRenderContext(runtime, options);
809
+ return await buildNginxBundleFromSource(await resolveRuntimeNginxBundleSource(runtime, options), options);
810
+ }
811
+ export async function buildManualEnvProxyNginxBundle(input, options) {
812
+ return await buildNginxBundleFromSource(await resolveManualNginxBundleSource(input), {
813
+ ...options,
814
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
815
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
816
+ });
817
+ }
818
+ async function buildNginxBundleFromSource(source, options) {
819
+ const context = await buildEnvProxyNginxRenderContext(source, options);
640
820
  const appTemplate = await readEnvProxyNginxAssetText('app.conf.tpl');
641
821
  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');
822
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
823
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
644
824
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
645
825
  readFile(sourceIndexV1Path, 'utf8'),
646
826
  readFile(sourceIndexV2Path, 'utf8'),
@@ -667,13 +847,13 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
667
847
  wsPath: context.wsPath,
668
848
  };
669
849
  return {
670
- envName: runtime.envName,
850
+ envName: source.envName,
671
851
  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 }),
852
+ entryDir: resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'nginx' }),
853
+ publicDir: resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope }),
854
+ appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
855
+ indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
856
+ indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
677
857
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
678
858
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
679
859
  appPublicPath: context.appPublicPath,
@@ -694,9 +874,19 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
694
874
  };
695
875
  }
696
876
  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');
877
+ return await buildCaddyBundleFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
878
+ }
879
+ export async function buildManualEnvProxyCaddyBundle(input, options) {
880
+ return await buildCaddyBundleFromSource(await resolveManualNginxBundleSource(input), {
881
+ ...options,
882
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
883
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
884
+ });
885
+ }
886
+ async function buildCaddyBundleFromSource(source, options) {
887
+ const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
888
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
889
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
700
890
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
701
891
  readFile(sourceIndexV1Path, 'utf8'),
702
892
  readFile(sourceIndexV2Path, 'utf8'),
@@ -707,9 +897,9 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
707
897
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
708
898
  const indexV1AssetPublicPath = context.cdnBaseUrl;
709
899
  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 });
900
+ const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
901
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
902
+ const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
713
903
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
714
904
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
715
905
  appPublicPath: context.appPublicPath,
@@ -725,13 +915,13 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
725
915
  wsPath: context.wsPath,
726
916
  }, renderedPublicDir);
727
917
  return {
728
- envName: runtime.envName,
918
+ envName: source.envName,
729
919
  envFilePath: context.envFilePath,
730
920
  entryDir,
731
921
  publicDir,
732
922
  appConfigPath,
733
- indexV1Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v1', { scope: options?.scope }),
734
- indexV2Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v2', { scope: options?.scope }),
923
+ indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
924
+ indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
735
925
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
736
926
  appPublicPath: context.appPublicPath,
737
927
  apiBasePath: context.apiBasePath,
@@ -893,20 +1083,23 @@ function buildNginxOtherLocation(appPublicPath, v2PublicPath, modernClientPrefix
893
1083
  function renderNginxLocationTemplate(context) {
894
1084
  const proxyPassBlock = buildNginxProxyPassBlock(context.proxyHost, context.apiPort);
895
1085
  const wsProxyPassTarget = `http://${context.proxyHost}:${context.apiPort}${context.wsPath}`;
1086
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
896
1087
  return ` location ~* ^${context.appPublicPath}storage/uploads/(.*\\.md)$ {
897
1088
  alias ${context.uploadsPath}/$1;
898
1089
  default_type text/markdown;
899
1090
  add_header Cache-Control "public";
900
1091
  add_header Content-Disposition "inline";
1092
+ add_header Content-Security-Policy "sandbox" always;
901
1093
  add_header X-Content-Type-Options "nosniff" always;
902
1094
  access_log off;
903
1095
  autoindex off;
904
1096
  }
905
1097
 
906
- location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|svg|svgz|xhtml|pdf))$ {
1098
+ location ~* ^${context.appPublicPath}storage/uploads/(.*\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt))$ {
907
1099
  alias ${context.uploadsPath}/$1;
908
1100
  add_header Cache-Control "public";
909
1101
  add_header Content-Disposition "attachment" always;
1102
+ add_header Content-Security-Policy "sandbox" always;
910
1103
  add_header X-Content-Type-Options "nosniff" always;
911
1104
  access_log off;
912
1105
  autoindex off;
@@ -915,6 +1108,7 @@ function renderNginxLocationTemplate(context) {
915
1108
  location ${context.appPublicPath}storage/uploads/ {
916
1109
  alias ${context.uploadsPath}/;
917
1110
  add_header Cache-Control "public";
1111
+ add_header Content-Security-Policy "sandbox" always;
918
1112
  add_header X-Content-Type-Options "nosniff" always;
919
1113
  access_log off;
920
1114
  autoindex off;
@@ -938,6 +1132,10 @@ function renderNginxLocationTemplate(context) {
938
1132
  ${proxyPassBlock}
939
1133
  }${context.otherLocation}
940
1134
 
1135
+ location = ${apiBasePathNoTrailingSlash} {
1136
+ return 308 ${context.apiBasePath}$is_args$args;
1137
+ }
1138
+
941
1139
  location ^~ ${context.apiBasePath} {
942
1140
  ${proxyPassBlock}
943
1141
  }
@@ -989,6 +1187,7 @@ function buildCaddyContextCommentLines(siteAddress, context, publicDir) {
989
1187
  }
990
1188
  function renderCaddyAppTemplate(siteAddress, context, publicDir) {
991
1189
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1190
+ const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
992
1191
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
993
1192
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
994
1193
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
@@ -1030,10 +1229,14 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1030
1229
  `${siteAddress} {`,
1031
1230
  ` encode zstd gzip${rootRedirectBlock}${appPublicPathRedirectBlock}${modernClientRedirectBlock}${shorthandModernClientRedirectBlock}`,
1032
1231
  '',
1232
+ ' @activeUploadedContent path_regexp activeUploadedContent (?i)\\.(?:htm|html|pdf|svg|svgz|xht|xhtml|xml|xsl|xslt)$',
1233
+ '',
1033
1234
  ` handle_path ${uploadsPathMatcher} {`,
1034
1235
  ` root * ${context.uploadsPath}`,
1035
1236
  ' header Cache-Control public',
1237
+ ' header Content-Security-Policy sandbox',
1036
1238
  ' header X-Content-Type-Options nosniff',
1239
+ ' header @activeUploadedContent Content-Disposition attachment',
1037
1240
  ' file_server',
1038
1241
  ' }',
1039
1242
  '',
@@ -1055,7 +1258,19 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1055
1258
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1056
1259
  ' }',
1057
1260
  '',
1058
- ' # Keep API and WS routes above the SPA fallbacks.',
1261
+ ' # Keep file, API and WS routes above the SPA fallbacks.',
1262
+ ` handle ${fileAccessPathMatcher} {`,
1263
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1264
+ ' }',
1265
+ ...(context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1266
+ ? []
1267
+ : [
1268
+ '',
1269
+ ' handle /files/* {',
1270
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1271
+ ' }',
1272
+ ]),
1273
+ '',
1059
1274
  ` handle ${apiPathMatcher} {`,
1060
1275
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1061
1276
  ' }',