@nocobase/cli 2.2.0-beta.8 → 2.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) 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 +21 -0
  11. package/dist/commands/app/start.js +4 -1
  12. package/dist/commands/env/info.js +11 -1
  13. package/dist/commands/init.js +11 -1
  14. package/dist/commands/install.js +61 -130
  15. package/dist/commands/proxy/caddy/generate.js +93 -7
  16. package/dist/commands/proxy/nginx/generate.js +98 -7
  17. package/dist/commands/revision/create.js +1 -1
  18. package/dist/commands/source/download.js +18 -14
  19. package/dist/lib/app-managed-resources.js +3 -2
  20. package/dist/lib/auth-store.js +68 -0
  21. package/dist/lib/cli-config.js +54 -1
  22. package/dist/lib/docker-image.js +94 -6
  23. package/dist/lib/env-config.js +5 -0
  24. package/dist/lib/env-proxy-config.js +48 -0
  25. package/dist/lib/env-proxy.js +193 -59
  26. package/dist/lib/prompt-catalog-terminal.js +32 -19
  27. package/dist/lib/prompt-web-ui.js +13 -2
  28. package/dist/lib/proxy-caddy.js +77 -9
  29. package/dist/lib/proxy-nginx.js +71 -11
  30. package/dist/locale/en-US.json +38 -38
  31. package/dist/locale/zh-CN.json +38 -38
  32. package/package.json +3 -2
@@ -259,7 +259,7 @@ export function applyEnvProxyAppEntryOptions(content, provider, options) {
259
259
  function toCaddyPathMatcher(prefixPath) {
260
260
  return `${prefixPath}*`;
261
261
  }
262
- export async function loadEnvProxySettings(runtime) {
262
+ export async function loadEnvProxySettings(runtime, options) {
263
263
  const { envFilePath, envValues } = await readManagedRuntimeEnvValues(runtime);
264
264
  const appPublicPath = resolveAppPublicPath(runtime.env.config.appPublicPath || envValues.APP_PUBLIC_PATH || DEFAULT_APP_PUBLIC_PATH);
265
265
  const apiClientShareToken = /^true$/i.test(String(envValues.API_CLIENT_SHARE_TOKEN ?? '').trim());
@@ -274,7 +274,9 @@ export async function loadEnvProxySettings(runtime) {
274
274
  wsPath: prefixRuntimePath(appPublicPath, envValues.WS_PATH || DEFAULT_WS_PATH),
275
275
  pluginStaticsPath: prefixRuntimePath(appPublicPath, envValues.PLUGIN_STATICS_PATH || DEFAULT_PLUGIN_STATICS_PATH, { trailingSlash: true }),
276
276
  modernClientPrefix: normalizeModernClientPrefix(envValues.APP_MODERN_CLIENT_PREFIX),
277
- cdnBaseUrl: trimValue(runtime.env.envVars?.CDN_BASE_URL) ?? trimValue(envValues.CDN_BASE_URL),
277
+ cdnBaseUrl: trimValue(options?.cdnBaseUrl) ??
278
+ trimValue(runtime.env.envVars?.CDN_BASE_URL) ??
279
+ trimValue(envValues.CDN_BASE_URL),
278
280
  apiClientStoragePrefix: trimValue(envValues.API_CLIENT_STORAGE_PREFIX) ?? DEFAULT_API_CLIENT_STORAGE_PREFIX,
279
281
  apiClientStorageType: trimValue(envValues.API_CLIENT_STORAGE_TYPE) ?? DEFAULT_API_CLIENT_STORAGE_TYPE,
280
282
  apiClientShareToken,
@@ -284,6 +286,53 @@ export async function loadEnvProxySettings(runtime) {
284
286
  },
285
287
  };
286
288
  }
289
+ function createManualProxyEnvSettings(input) {
290
+ const appPublicPath = resolveAppPublicPath(input.appPublicPath || DEFAULT_APP_PUBLIC_PATH);
291
+ return {
292
+ appPublicPath,
293
+ apiBasePath: prefixRuntimePath(appPublicPath, DEFAULT_API_BASE_PATH, {
294
+ trailingSlash: true,
295
+ }),
296
+ distPath: resolveDistPublicPath(appPublicPath),
297
+ wsPath: prefixRuntimePath(appPublicPath, DEFAULT_WS_PATH),
298
+ pluginStaticsPath: prefixRuntimePath(appPublicPath, DEFAULT_PLUGIN_STATICS_PATH, {
299
+ trailingSlash: true,
300
+ }),
301
+ modernClientPrefix: DEFAULT_MODERN_CLIENT_PREFIX,
302
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
303
+ apiClientStoragePrefix: DEFAULT_API_CLIENT_STORAGE_PREFIX,
304
+ apiClientStorageType: DEFAULT_API_CLIENT_STORAGE_TYPE,
305
+ apiClientShareToken: false,
306
+ wsUrl: '',
307
+ esmCdnBaseUrl: DEFAULT_ESM_CDN_BASE_URL,
308
+ esmCdnSuffix: '',
309
+ };
310
+ }
311
+ function normalizeManualNginxInput(input) {
312
+ const upstreamPort = trimValue(input.upstreamPort) ?? trimValue(input.appPort);
313
+ return {
314
+ name: String(input.name).trim(),
315
+ storagePath: String(input.storagePath).trim(),
316
+ distRootPath: String(input.distRootPath).trim(),
317
+ runtimeVersion: String(input.runtimeVersion).trim(),
318
+ appPublicPath: trimValue(input.appPublicPath),
319
+ upstreamHost: trimValue(input.upstreamHost),
320
+ upstreamPort,
321
+ appPort: trimValue(input.appPort),
322
+ cdnBaseUrl: trimValue(input.cdnBaseUrl),
323
+ };
324
+ }
325
+ function normalizeProxyPort(value) {
326
+ const normalized = trimValue(value);
327
+ if (!normalized || !/^\d+$/.test(normalized)) {
328
+ return undefined;
329
+ }
330
+ const port = Number(normalized);
331
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
332
+ return undefined;
333
+ }
334
+ return normalized;
335
+ }
287
336
  async function parseVersionFromPackageJson(content, sourceLabel) {
288
337
  let parsed;
289
338
  try {
@@ -438,7 +487,9 @@ function rewriteHtmlAssetPublicPath(html, currentPublicPath, nextPublicPath) {
438
487
  }
439
488
  function buildNginxManagedConfigBlock(context) {
440
489
  const v2PublicPathNoTrailingSlash = trimTrailingSlash(context.v2PublicPath);
490
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
441
491
  const appPublicPathNoTrailingSlash = trimTrailingSlash(context.appPublicPath);
492
+ const fileAccessPath = `${context.appPublicPath}files/`;
442
493
  const isRootMounted = context.appPublicPath === '/';
443
494
  const appPublicPathRedirectBlock = isRootMounted
444
495
  ? ''
@@ -476,6 +527,24 @@ function buildNginxManagedConfigBlock(context) {
476
527
  ` include ${context.snippetsDir}/proxy-location.conf;`,
477
528
  ' }',
478
529
  '',
530
+ ` location ^~ ${fileAccessPath} {`,
531
+ ` proxy_pass ${context.backendUrl};`,
532
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
533
+ ' }',
534
+ ...(!isRootMounted
535
+ ? [
536
+ '',
537
+ ' location ^~ /files/ {',
538
+ ` proxy_pass ${context.backendUrl};`,
539
+ ` include ${context.snippetsDir}/proxy-location.conf;`,
540
+ ' }',
541
+ ]
542
+ : []),
543
+ '',
544
+ ` location = ${apiBasePathNoTrailingSlash} {`,
545
+ ` return 308 ${context.apiBasePath}$is_args$args;`,
546
+ ' }',
547
+ '',
479
548
  ` location ^~ ${context.apiBasePath} {`,
480
549
  ` proxy_pass ${context.backendUrl};`,
481
550
  ` include ${context.snippetsDir}/proxy-location.conf;`,
@@ -525,65 +594,92 @@ function buildNginxRuntimeConfig(context, variant) {
525
594
  function buildCaddyRuntimeConfig(context, variant) {
526
595
  return buildNginxRuntimeConfig(context, variant);
527
596
  }
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);
597
+ async function buildEnvProxyNginxRenderContext(source, options) {
542
598
  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 });
599
+ const upstreamPort = normalizeProxyPort(options?.upstreamPort) ?? source.apiPort;
600
+ const backendUrl = `http://${proxyHost}:${upstreamPort}`;
601
+ const cdnBaseUrl = source.settings.cdnBaseUrl ?? buildDefaultCdnBaseUrl(source.settings.appPublicPath, source.activeVersion);
602
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope });
603
+ const publicDir = resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope });
547
604
  const snippetsDir = resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope });
548
- const distRootDir = resolveDistClientRoot(runtime.env.storagePath);
549
- const uploadsDir = path.join(runtime.env.storagePath, 'uploads');
605
+ const distRootDir = source.distRootPath;
606
+ const uploadsDir = path.join(source.storagePath, 'uploads');
550
607
  const mappedEntryDir = await mapProxyPathFromCliRoot(entryDir, options);
551
608
  const mappedPublicDir = await mapProxyPathFromCliRoot(publicDir, options);
552
609
  const mappedSnippetsDir = await mapProxyPathFromCliRoot(snippetsDir, options);
553
610
  const mappedDistRootDir = await mapProxyPathFromCliRoot(distRootDir, options);
554
611
  const mappedUploadsDir = await mapProxyPathFromCliRoot(uploadsDir, options);
555
- const v2PublicPath = `${settings.appPublicPath.replace(/\/$/, '')}/${settings.modernClientPrefix}/`;
612
+ const v2PublicPath = `${source.settings.appPublicPath.replace(/\/$/, '')}/${source.settings.modernClientPrefix}/`;
556
613
  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,
614
+ envName: source.envName,
615
+ envFilePath: source.envFilePath,
616
+ apiBasePath: source.settings.apiBasePath,
617
+ apiClientShareToken: source.settings.apiClientShareToken,
618
+ apiClientStoragePrefix: source.settings.apiClientStoragePrefix,
619
+ apiClientStorageType: source.settings.apiClientStorageType,
620
+ apiPort: source.apiPort,
621
+ appPublicPath: source.settings.appPublicPath,
565
622
  backendUrl,
566
623
  cdnBaseUrl: ensureTrailingSlash(cdnBaseUrl),
567
- distPath: settings.distPath,
624
+ distPath: source.settings.distPath,
568
625
  distRootDir: mappedDistRootDir,
569
626
  entryDir: mappedEntryDir,
570
627
  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,
628
+ esmCdnBaseUrl: source.settings.esmCdnBaseUrl,
629
+ esmCdnSuffix: source.settings.esmCdnSuffix,
630
+ indexV1Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }), options),
631
+ indexV2Path: await mapProxyPathFromCliRoot(resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }), options),
632
+ modernClientPrefix: source.settings.modernClientPrefix,
576
633
  proxyHost,
577
634
  snippetsDir: mappedSnippetsDir,
578
635
  uploadsDir: mappedUploadsDir,
579
636
  v2PublicPath,
580
- wsPath: settings.wsPath,
581
- wsUrl: settings.wsUrl,
637
+ wsPath: source.settings.wsPath,
638
+ wsUrl: source.settings.wsUrl,
639
+ activeVersion: source.activeVersion,
640
+ };
641
+ }
642
+ async function resolveRuntimeNginxBundleSource(runtime, options) {
643
+ const apiPort = trimValue(runtime.env.appPort ?? runtime.env.config.appPort);
644
+ if (!apiPort) {
645
+ throw new Error(translateCli('commands.envProxy.errors.missingAppPort', { envName: runtime.envName }, {
646
+ fallback: `Missing appPort for env "${runtime.envName}". Save or update the app port before generating proxy config.`,
647
+ }));
648
+ }
649
+ const activeVersion = trimValue(await readDistClientActiveVersion(runtime.env.storagePath));
650
+ if (!activeVersion) {
651
+ throw new Error(translateCli('commands.envProxy.errors.missingVersion', { envName: runtime.envName }, {
652
+ fallback: `Couldn't determine the app version for env "${runtime.envName}". Run \`nb env update ${runtime.envName}\` and try again.`,
653
+ }));
654
+ }
655
+ const { envFilePath, settings } = await loadEnvProxySettings(runtime, options);
656
+ return {
657
+ envName: runtime.envName,
658
+ envFilePath,
659
+ storagePath: runtime.env.storagePath,
660
+ distRootPath: resolveDistClientRoot(runtime.env.storagePath),
661
+ settings,
662
+ apiPort,
582
663
  activeVersion,
583
664
  };
584
665
  }
666
+ async function resolveManualNginxBundleSource(input) {
667
+ const normalized = normalizeManualNginxInput(input);
668
+ return {
669
+ envName: normalized.name,
670
+ envFilePath: undefined,
671
+ storagePath: normalized.storagePath,
672
+ distRootPath: normalized.distRootPath,
673
+ settings: createManualProxyEnvSettings(normalized),
674
+ apiPort: normalized.upstreamPort,
675
+ activeVersion: normalized.runtimeVersion,
676
+ };
677
+ }
585
678
  async function buildEnvProxyCaddyRenderContext(runtime, options) {
586
- return await buildEnvProxyNginxRenderContext(runtime, {
679
+ return await buildEnvProxyCaddyRenderContextFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
680
+ }
681
+ async function buildEnvProxyCaddyRenderContextFromSource(source, options) {
682
+ return await buildEnvProxyNginxRenderContext(source, {
587
683
  ...options,
588
684
  provider: 'caddy',
589
685
  });
@@ -636,11 +732,21 @@ export function resolveEnvProxyCaddyIndexOutputPath(envName, variant, options) {
636
732
  return path.join(resolveEnvProxyCaddyPublicOutputDir(envName, { scope: options?.scope }), `index-${variant}.html`);
637
733
  }
638
734
  export async function buildEnvProxyNginxBundle(runtime, options) {
639
- const context = await buildEnvProxyNginxRenderContext(runtime, options);
735
+ return await buildNginxBundleFromSource(await resolveRuntimeNginxBundleSource(runtime, options), options);
736
+ }
737
+ export async function buildManualEnvProxyNginxBundle(input, options) {
738
+ return await buildNginxBundleFromSource(await resolveManualNginxBundleSource(input), {
739
+ ...options,
740
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
741
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
742
+ });
743
+ }
744
+ async function buildNginxBundleFromSource(source, options) {
745
+ const context = await buildEnvProxyNginxRenderContext(source, options);
640
746
  const appTemplate = await readEnvProxyNginxAssetText('app.conf.tpl');
641
747
  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');
748
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
749
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
644
750
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
645
751
  readFile(sourceIndexV1Path, 'utf8'),
646
752
  readFile(sourceIndexV2Path, 'utf8'),
@@ -667,13 +773,13 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
667
773
  wsPath: context.wsPath,
668
774
  };
669
775
  return {
670
- envName: runtime.envName,
776
+ envName: source.envName,
671
777
  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 }),
778
+ entryDir: resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'nginx' }),
779
+ publicDir: resolveEnvProxyNginxPublicOutputDir(source.envName, { scope: options?.scope }),
780
+ appConfigPath: resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'nginx' }),
781
+ indexV1Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
782
+ indexV2Path: resolveEnvProxyNginxIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
677
783
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'nginx' }),
678
784
  snippetsDir: resolveEnvProxyNginxSnippetsOutputDir({ scope: options?.scope }),
679
785
  appPublicPath: context.appPublicPath,
@@ -694,9 +800,19 @@ export async function buildEnvProxyNginxBundle(runtime, options) {
694
800
  };
695
801
  }
696
802
  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');
803
+ return await buildCaddyBundleFromSource(await resolveRuntimeNginxBundleSource(runtime), options);
804
+ }
805
+ export async function buildManualEnvProxyCaddyBundle(input, options) {
806
+ return await buildCaddyBundleFromSource(await resolveManualNginxBundleSource(input), {
807
+ ...options,
808
+ upstreamHost: trimValue(input.upstreamHost) ?? options?.upstreamHost,
809
+ upstreamPort: normalizeProxyPort(input.upstreamPort) ?? options?.upstreamPort,
810
+ });
811
+ }
812
+ async function buildCaddyBundleFromSource(source, options) {
813
+ const context = await buildEnvProxyCaddyRenderContextFromSource(source, options);
814
+ const sourceIndexV1Path = path.join(source.distRootPath, context.activeVersion, 'index.html');
815
+ const sourceIndexV2Path = path.join(source.distRootPath, context.activeVersion, DEFAULT_MODERN_CLIENT_PREFIX, 'index.html');
700
816
  const [sourceIndexV1Content, sourceIndexV2Content] = await Promise.all([
701
817
  readFile(sourceIndexV1Path, 'utf8'),
702
818
  readFile(sourceIndexV2Path, 'utf8'),
@@ -707,9 +823,9 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
707
823
  const sourceV2PublicPath = extractRuntimePublicPath(sourceIndexV2Content);
708
824
  const indexV1AssetPublicPath = context.cdnBaseUrl;
709
825
  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 });
826
+ const appConfigPath = resolveEnvProxyAppOutputPath(source.envName, { scope: options?.scope, provider: 'caddy' });
827
+ const entryDir = resolveEnvProxyEntryDir(source.envName, { scope: options?.scope, provider: 'caddy' });
828
+ const publicDir = resolveEnvProxyCaddyPublicOutputDir(source.envName, { scope: options?.scope });
713
829
  const renderedPublicDir = await mapProxyPathFromCliRoot(publicDir, { ...options, provider: 'caddy' });
714
830
  const appConfigContent = renderCaddyAppTemplate(buildCaddySiteAddress(), {
715
831
  appPublicPath: context.appPublicPath,
@@ -725,13 +841,13 @@ export async function buildEnvProxyCaddyBundle(runtime, options) {
725
841
  wsPath: context.wsPath,
726
842
  }, renderedPublicDir);
727
843
  return {
728
- envName: runtime.envName,
844
+ envName: source.envName,
729
845
  envFilePath: context.envFilePath,
730
846
  entryDir,
731
847
  publicDir,
732
848
  appConfigPath,
733
- indexV1Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v1', { scope: options?.scope }),
734
- indexV2Path: resolveEnvProxyCaddyIndexOutputPath(runtime.envName, 'v2', { scope: options?.scope }),
849
+ indexV1Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v1', { scope: options?.scope }),
850
+ indexV2Path: resolveEnvProxyCaddyIndexOutputPath(source.envName, 'v2', { scope: options?.scope }),
735
851
  mainConfigPath: resolveEnvProxyMainOutputPath({ scope: options?.scope, provider: 'caddy' }),
736
852
  appPublicPath: context.appPublicPath,
737
853
  apiBasePath: context.apiBasePath,
@@ -893,6 +1009,7 @@ function buildNginxOtherLocation(appPublicPath, v2PublicPath, modernClientPrefix
893
1009
  function renderNginxLocationTemplate(context) {
894
1010
  const proxyPassBlock = buildNginxProxyPassBlock(context.proxyHost, context.apiPort);
895
1011
  const wsProxyPassTarget = `http://${context.proxyHost}:${context.apiPort}${context.wsPath}`;
1012
+ const apiBasePathNoTrailingSlash = trimTrailingSlash(context.apiBasePath);
896
1013
  return ` location ~* ^${context.appPublicPath}storage/uploads/(.*\\.md)$ {
897
1014
  alias ${context.uploadsPath}/$1;
898
1015
  default_type text/markdown;
@@ -938,6 +1055,10 @@ function renderNginxLocationTemplate(context) {
938
1055
  ${proxyPassBlock}
939
1056
  }${context.otherLocation}
940
1057
 
1058
+ location = ${apiBasePathNoTrailingSlash} {
1059
+ return 308 ${context.apiBasePath}$is_args$args;
1060
+ }
1061
+
941
1062
  location ^~ ${context.apiBasePath} {
942
1063
  ${proxyPassBlock}
943
1064
  }
@@ -989,6 +1110,7 @@ function buildCaddyContextCommentLines(siteAddress, context, publicDir) {
989
1110
  }
990
1111
  function renderCaddyAppTemplate(siteAddress, context, publicDir) {
991
1112
  const uploadsPath = `${context.appPublicPath}storage/uploads/`;
1113
+ const fileAccessPathMatcher = toCaddyPathMatcher(`${context.appPublicPath}files/`);
992
1114
  const distPathMatcher = toCaddyPathMatcher(context.distPath);
993
1115
  const uploadsPathMatcher = toCaddyPathMatcher(uploadsPath);
994
1116
  const apiPathMatcher = toCaddyPathMatcher(context.apiBasePath);
@@ -1055,7 +1177,19 @@ function renderCaddyAppTemplate(siteAddress, context, publicDir) {
1055
1177
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1056
1178
  ' }',
1057
1179
  '',
1058
- ' # Keep API and WS routes above the SPA fallbacks.',
1180
+ ' # Keep file, API and WS routes above the SPA fallbacks.',
1181
+ ` handle ${fileAccessPathMatcher} {`,
1182
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1183
+ ' }',
1184
+ ...(context.appPublicPath === DEFAULT_APP_PUBLIC_PATH
1185
+ ? []
1186
+ : [
1187
+ '',
1188
+ ' handle /files/* {',
1189
+ ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1190
+ ' }',
1191
+ ]),
1192
+ '',
1059
1193
  ` handle ${apiPathMatcher} {`,
1060
1194
  ` reverse_proxy ${context.proxyHost}:${context.apiPort}`,
1061
1195
  ' }',
@@ -11,6 +11,17 @@ import { exit, stdin as stdinStream, stdout as stdoutStream } from 'node:process
11
11
  import { createCliTranslate } from "./cli-locale.js";
12
12
  import { confirm, select, input, password } from "./inquirer.js";
13
13
  import { createPromptCatalogHooks, hasIvKey, isBlankText, isPromptBlockSkipped, mergedBoolean, mergedInteger, mergedPassword, mergedSelect, mergedText, resolvePromptCatalogLocale, resolvePromptText, runPromptFieldValidate, selectOptionValues, tryApplyPreset, } from "./prompt-catalog-core.js";
14
+ function buildPromptComputationSeed(catalog, initialValues) {
15
+ const catalogKeys = new Set(Object.keys(catalog));
16
+ const seed = {};
17
+ for (const [key, value] of Object.entries(initialValues)) {
18
+ if (catalogKeys.has(key) || value === undefined || value === null) {
19
+ continue;
20
+ }
21
+ seed[key] = value;
22
+ }
23
+ return seed;
24
+ }
14
25
  function adaptInquirerValidate(validate) {
15
26
  if (!validate) {
16
27
  return undefined;
@@ -113,14 +124,16 @@ export async function runPromptCatalog(catalog, options = {}) {
113
124
  const hooks = createTerminalHooks(locale, options.hooks);
114
125
  const interactive = Boolean(stdinStream.isTTY && stdoutStream.isTTY && !options.yes);
115
126
  const preset = options.values ?? {};
127
+ const computationSeed = buildPromptComputationSeed(catalog, resolveIv);
116
128
  const out = {};
117
129
  const renderer = createInquirerRenderer();
118
130
  for (const [key, def] of Object.entries(catalog)) {
119
- if (isPromptBlockSkipped(def, out)) {
131
+ const valuesSoFar = { ...computationSeed, ...out };
132
+ if (isPromptBlockSkipped(def, valuesSoFar)) {
120
133
  continue;
121
134
  }
122
135
  if (tryApplyPreset(key, def, preset, out, hooks, locale)) {
123
- const errV = await runPromptFieldValidate(def, out[key], out);
136
+ const errV = await runPromptFieldValidate(def, out[key], { ...computationSeed, ...out });
124
137
  if (errV) {
125
138
  hooks.onMissingNonInteractive(errV);
126
139
  }
@@ -135,7 +148,7 @@ export async function runPromptCatalog(catalog, options = {}) {
135
148
  continue;
136
149
  }
137
150
  if (def.type === 'run') {
138
- await def.run(out, options.command);
151
+ await def.run({ ...computationSeed, ...out }, options.command);
139
152
  continue;
140
153
  }
141
154
  if (def.type === 'text') {
@@ -144,18 +157,18 @@ export async function runPromptCatalog(catalog, options = {}) {
144
157
  ? resolvePromptText(def.placeholder, locale)
145
158
  : undefined;
146
159
  if (!interactive) {
147
- const merged = mergedText(key, def, resolveIv, useYesInitial, out);
160
+ const merged = mergedText(key, def, resolveIv, useYesInitial, valuesSoFar);
148
161
  if (def.required && isBlankText(merged)) {
149
162
  hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.textRequired', { key }));
150
163
  }
151
164
  out[key] = merged;
152
- const errT = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
165
+ const errT = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
153
166
  if (errT) {
154
167
  hooks.onMissingNonInteractive(errT);
155
168
  }
156
169
  continue;
157
170
  }
158
- const merged = mergedText(key, def, promptIv, false, out);
171
+ const merged = mergedText(key, def, promptIv, false, valuesSoFar);
159
172
  const raw = await callPrompt(() => renderer.text({
160
173
  message,
161
174
  initialValue: merged,
@@ -168,7 +181,7 @@ export async function runPromptCatalog(catalog, options = {}) {
168
181
  return undefined;
169
182
  }
170
183
  const currentValue = typeof value === 'string' ? value : String(value ?? '');
171
- const result = runPromptFieldValidate(def, currentValue, { ...out, [key]: currentValue });
184
+ const result = runPromptFieldValidate(def, currentValue, { ...computationSeed, ...out, [key]: currentValue });
172
185
  return result;
173
186
  },
174
187
  }), renderer, hooks);
@@ -180,7 +193,7 @@ export async function runPromptCatalog(catalog, options = {}) {
180
193
  if (!interactive) {
181
194
  const b = mergedBoolean(key, def, resolveIv, useYesInitial);
182
195
  out[key] = b;
183
- const errB = await runPromptFieldValidate(def, b, { ...out, [key]: b });
196
+ const errB = await runPromptFieldValidate(def, b, { ...computationSeed, ...out, [key]: b });
184
197
  if (errB) {
185
198
  hooks.onMissingNonInteractive(errB);
186
199
  }
@@ -191,7 +204,7 @@ export async function runPromptCatalog(catalog, options = {}) {
191
204
  for (;;) {
192
205
  const raw = await callPrompt(() => renderer.confirm({ message, initialValue: merged }), renderer, hooks);
193
206
  const b = Boolean(raw);
194
- const errB = await runPromptFieldValidate(def, b, { ...out, [key]: b });
207
+ const errB = await runPromptFieldValidate(def, b, { ...computationSeed, ...out, [key]: b });
195
208
  if (errB) {
196
209
  renderer.error(errB);
197
210
  continue;
@@ -224,7 +237,7 @@ export async function runPromptCatalog(catalog, options = {}) {
224
237
  : t('promptCatalog.nonInteractive.selectMissingDefault', { key }));
225
238
  }
226
239
  out[key] = merged;
227
- const errS = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
240
+ const errS = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
228
241
  if (errS) {
229
242
  hooks.onMissingNonInteractive(errS);
230
243
  }
@@ -248,7 +261,7 @@ export async function runPromptCatalog(catalog, options = {}) {
248
261
  initialValue: uiInitial,
249
262
  }), renderer, hooks);
250
263
  const picked = raw;
251
- const errS = await runPromptFieldValidate(def, picked, { ...out, [key]: picked });
264
+ const errS = await runPromptFieldValidate(def, picked, { ...computationSeed, ...out, [key]: picked });
252
265
  if (errS) {
253
266
  renderer.error(errS);
254
267
  continue;
@@ -269,13 +282,13 @@ export async function runPromptCatalog(catalog, options = {}) {
269
282
  if (def.type === 'password') {
270
283
  const message = resolvePromptText(def.message, locale, key);
271
284
  if (!interactive) {
272
- const merged = mergedPassword(key, def, resolveIv, useYesInitial);
285
+ const merged = mergedPassword(key, def, resolveIv, useYesInitial, valuesSoFar);
273
286
  if (merged === undefined) {
274
287
  if (def.required) {
275
288
  hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequired', { key }));
276
289
  }
277
290
  out[key] = '';
278
- const errPE = await runPromptFieldValidate(def, '', { ...out, [key]: '' });
291
+ const errPE = await runPromptFieldValidate(def, '', { ...computationSeed, ...out, [key]: '' });
279
292
  if (errPE) {
280
293
  hooks.onMissingNonInteractive(errPE);
281
294
  }
@@ -285,7 +298,7 @@ export async function runPromptCatalog(catalog, options = {}) {
285
298
  hooks.onMissingNonInteractive(t('promptCatalog.nonInteractive.passwordRequiredNonEmpty', { key }));
286
299
  }
287
300
  out[key] = merged;
288
- const errP = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
301
+ const errP = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
289
302
  if (errP) {
290
303
  hooks.onMissingNonInteractive(errP);
291
304
  }
@@ -302,7 +315,7 @@ export async function runPromptCatalog(catalog, options = {}) {
302
315
  return undefined;
303
316
  }
304
317
  const currentValue = typeof value === 'string' ? value : String(value ?? '');
305
- const result = runPromptFieldValidate(def, currentValue, { ...out, [key]: currentValue });
318
+ const result = runPromptFieldValidate(def, currentValue, { ...computationSeed, ...out, [key]: currentValue });
306
319
  return result;
307
320
  },
308
321
  }), renderer, hooks);
@@ -322,14 +335,14 @@ export async function runPromptCatalog(catalog, options = {}) {
322
335
  }
323
336
  const z = def.initialValue ?? 0;
324
337
  out[key] = z;
325
- const errI = await runPromptFieldValidate(def, z, { ...out, [key]: z });
338
+ const errI = await runPromptFieldValidate(def, z, { ...computationSeed, ...out, [key]: z });
326
339
  if (errI) {
327
340
  hooks.onMissingNonInteractive(errI);
328
341
  }
329
342
  continue;
330
343
  }
331
344
  out[key] = merged;
332
- const errI2 = await runPromptFieldValidate(def, merged, { ...out, [key]: merged });
345
+ const errI2 = await runPromptFieldValidate(def, merged, { ...computationSeed, ...out, [key]: merged });
333
346
  if (errI2) {
334
347
  hooks.onMissingNonInteractive(errI2);
335
348
  }
@@ -349,7 +362,7 @@ export async function runPromptCatalog(catalog, options = {}) {
349
362
  }
350
363
  if (def.validate) {
351
364
  const z = def.initialValue ?? 0;
352
- return runPromptFieldValidate(def, z, { ...out, [key]: z });
365
+ return runPromptFieldValidate(def, z, { ...computationSeed, ...out, [key]: z });
353
366
  }
354
367
  return undefined;
355
368
  }
@@ -360,7 +373,7 @@ export async function runPromptCatalog(catalog, options = {}) {
360
373
  return undefined;
361
374
  }
362
375
  const n = Number.parseInt(trimmed, 10);
363
- return runPromptFieldValidate(def, n, { ...out, [key]: n });
376
+ return runPromptFieldValidate(def, n, { ...computationSeed, ...out, [key]: n });
364
377
  },
365
378
  }), renderer, hooks);
366
379
  if (typeof raw === 'string' && raw.trim() === '' && !def.required) {
@@ -48,13 +48,24 @@ function isInputBlock(def) {
48
48
  def.type === 'password' ||
49
49
  def.type === 'integer');
50
50
  }
51
+ function buildPromptComputationSeed(catalog, userPreset) {
52
+ const catalogKeys = new Set(Object.keys(catalog));
53
+ const seed = {};
54
+ for (const [key, value] of Object.entries(userPreset)) {
55
+ if (catalogKeys.has(key) || value === undefined || value === null) {
56
+ continue;
57
+ }
58
+ seed[key] = value;
59
+ }
60
+ return seed;
61
+ }
51
62
  /**
52
63
  * Merges CLI/env **`userPreset`** with catalog block defaults, in the same key order and with the
53
64
  * same `hidden` / `run` semantics as {@link isPromptBlockSkipped}, so the web form can prefill
54
65
  * and reflow `hidden` fields (e.g. `integer` when `select` changes).
55
66
  */
56
67
  export function buildWebFormValuesFromCatalog(catalog, userPreset = {}) {
57
- const out = {};
68
+ const out = buildPromptComputationSeed(catalog, userPreset);
58
69
  for (const [key, def] of Object.entries(catalog)) {
59
70
  if (def.type === 'intro' || def.type === 'outro') {
60
71
  continue;
@@ -109,7 +120,7 @@ function defaultValueForInput(key, def, out) {
109
120
  * from current raw form data (e.g. after changing `select`). Matches how {@link isPromptBlockSkipped} uses `out` while iterating the catalog.
110
121
  */
111
122
  export function reflowWebFormState(catalog, raw, userSeed = {}) {
112
- const out = {};
123
+ const out = buildPromptComputationSeed(catalog, userSeed);
113
124
  const show = {};
114
125
  for (const [key, def] of Object.entries(catalog)) {
115
126
  if (def.type === 'intro' || def.type === 'outro') {