@bleedingdev/modern-js-create 3.2.0-ultramodern.21 → 3.2.0-ultramodern.22

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.
package/dist/index.js CHANGED
@@ -468,12 +468,13 @@ const EN_LOCALE = {
468
468
  optionRouter: ' -r, --router Select router framework (react-router or tanstack)',
469
469
  optionBff: ' --bff Enable BFF scaffold (default runtime: effect)',
470
470
  optionBffRuntime: ' --bff-runtime Select BFF runtime (hono or effect)',
471
- optionTailwind: ' --tailwind Enable Tailwind CSS v4 scaffold (PostCSS + starter styles)',
471
+ optionTailwind: ' --no-tailwind Disable default Tailwind CSS v4 scaffold',
472
472
  optionWorkspace: ' --workspace Use workspace protocol for @modern-js dependencies (for local monorepo testing)',
473
473
  optionUltramodernWorkspace: ' --ultramodern-workspace Generate the canonical UltraModern SuperApp workspace',
474
474
  optionUltramodernPackageSource: ' --ultramodern-package-source Select UltraModern package source (workspace or install)',
475
475
  optionUltramodernPackageScope: ' --ultramodern-package-scope Publish scope for npm alias installs (for example bleedingdev)',
476
476
  optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix Prefix for npm alias package names (default: modern-js-)',
477
+ optionMicroVertical: ' --microvertical Add a MicroVertical package to an existing UltraModern workspace (remote, horizontal-remote, service, shared)',
477
478
  optionSub: ' -s, --sub Mark as a subproject (package in monorepo)',
478
479
  examples: '💡 Examples:',
479
480
  example1: ' create my-app',
@@ -481,11 +482,12 @@ const EN_LOCALE = {
481
482
  example3: ' create my-app --sub',
482
483
  example4: ' create --help',
483
484
  example5: ' create my-app --router tanstack',
484
- example6: ' create my-app --router tanstack --tailwind',
485
+ example6: ' create my-app --router tanstack --no-tailwind',
485
486
  example7: ' create my-app --bff',
486
487
  example8: ' create my-app --router tanstack --bff-runtime effect',
487
488
  example9: ' create my-app --router tanstack --bff-runtime effect --workspace',
488
489
  example10: ' pnpm dlx @bleedingdev/modern-js-create my-super-app',
490
+ example11: ' create catalog --microvertical remote',
489
491
  moreInfo: '📚 Learn more: https://modernjs.dev'
490
492
  },
491
493
  version: {
@@ -523,12 +525,13 @@ const ZH_LOCALE = {
523
525
  optionRouter: ' -r, --router 选择路由框架 (react-router 或 tanstack)',
524
526
  optionBff: ' --bff 启用 BFF 模板(默认运行时:effect)',
525
527
  optionBffRuntime: ' --bff-runtime 选择 BFF 运行时(hono 或 effect)',
526
- optionTailwind: ' --tailwind 启用 Tailwind CSS v4 模板(PostCSS + 示例样式)',
528
+ optionTailwind: ' --no-tailwind 禁用默认 Tailwind CSS v4 模板',
527
529
  optionWorkspace: ' --workspace 对 @modern-js 依赖使用 workspace 协议(用于本地 monorepo 联调)',
528
530
  optionUltramodernWorkspace: ' --ultramodern-workspace 生成标准 UltraModern SuperApp 工作区',
529
531
  optionUltramodernPackageSource: ' --ultramodern-package-source 选择 UltraModern 依赖来源(workspace 或 install)',
530
532
  optionUltramodernPackageScope: ' --ultramodern-package-scope npm alias 安装使用的发布 scope(例如 bleedingdev)',
531
533
  optionUltramodernPackageNamePrefix: ' --ultramodern-package-name-prefix npm alias 包名前缀(默认:modern-js-)',
534
+ optionMicroVertical: ' --microvertical 向现有 UltraModern 工作区添加 MicroVertical 包(remote、horizontal-remote、service、shared)',
532
535
  optionSub: ' -s, --sub 标记为子项目(monorepo 中的子包)',
533
536
  examples: '💡 示例:',
534
537
  example1: ' create my-app',
@@ -536,11 +539,12 @@ const ZH_LOCALE = {
536
539
  example3: ' create my-app --sub',
537
540
  example4: ' create --help',
538
541
  example5: ' create my-app --router tanstack',
539
- example6: ' create my-app --router tanstack --tailwind',
542
+ example6: ' create my-app --router tanstack --no-tailwind',
540
543
  example7: ' create my-app --bff',
541
544
  example8: ' create my-app --router tanstack --bff-runtime effect',
542
545
  example9: ' create my-app --router tanstack --bff-runtime effect --workspace',
543
546
  example10: ' create my-super-app --ultramodern-workspace --ultramodern-package-source install --ultramodern-package-scope bleedingdev',
547
+ example11: ' create catalog --microvertical remote',
544
548
  moreInfo: '📚 更多信息: https://modernjs.dev'
545
549
  },
546
550
  version: {
@@ -554,20 +558,26 @@ const localeKeys = i18n.init('en', {
554
558
  });
555
559
  const ultramodern_workspace_dirname = node_path.dirname(fileURLToPath(import.meta.url));
556
560
  const workspaceTemplateDir = node_path.resolve(ultramodern_workspace_dirname, '..', 'template-workspace');
557
- const TANSTACK_ROUTER_VERSION = '1.170.1';
558
- const MODULE_FEDERATION_VERSION = '2.4.0';
559
- const ZEPHYR_MODERNJS_PLUGIN_VERSION = '1.1.1';
560
- const EFFECT_TSGO_VERSION = '0.7.3';
561
- const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260518.1';
562
- const OXLINT_VERSION = '1.65.0';
563
- const OXFMT_VERSION = '0.50.0';
561
+ const TANSTACK_ROUTER_VERSION = '1.170.8';
562
+ const MODULE_FEDERATION_VERSION = '2.5.0';
563
+ const ZEPHYR_RSPACK_PLUGIN_VERSION = '1.1.1';
564
+ const ZEPHYR_AGENT_VERSION = '1.1.1';
565
+ const WRANGLER_VERSION = '4.95.0';
566
+ const TAILWIND_VERSION = '4.3.0';
567
+ const TAILWIND_POSTCSS_VERSION = '4.3.0';
568
+ const EFFECT_TSGO_VERSION = '0.11.0';
569
+ const TYPESCRIPT_VERSION = '6.0.3';
570
+ const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260526.1';
571
+ const OXLINT_VERSION = '1.66.0';
572
+ const OXFMT_VERSION = '0.51.0';
564
573
  const ULTRACITE_VERSION = '7.7.0';
565
574
  const I18NEXT_VERSION = '26.2.0';
566
575
  const REACT_VERSION = '^19.2.6';
567
576
  const REACT_DOM_VERSION = '^19.2.6';
568
- const REACT_I18NEXT_VERSION = '17.0.8';
569
577
  const WORKSPACE_PACKAGE_VERSION = 'workspace:*';
578
+ const GENERATED_CONTRACT_PATH = '.modernjs/ultramodern-generated-contract.json';
570
579
  const RSTACK_AGENT_SKILLS_COMMIT = '61c948b42512e223bad44b83af4080eba48b2677';
580
+ const MODULE_FEDERATION_AGENT_SKILLS_COMMIT = '07bb5b6c43ad457609e00c081b72d4c42508ec76';
571
581
  const baselineAgentSkills = [
572
582
  'rsbuild-best-practices',
573
583
  'rspack-best-practices',
@@ -577,6 +587,9 @@ const baselineAgentSkills = [
577
587
  'rslib-modern-package',
578
588
  'rstest-best-practices'
579
589
  ];
590
+ const moduleFederationAgentSkills = [
591
+ 'mf'
592
+ ];
580
593
  const privateAgentSkills = [
581
594
  'plan-graph',
582
595
  'dag',
@@ -637,6 +650,14 @@ const remoteApps = [
637
650
  './Route': './src/remote-entry.tsx',
638
651
  './Widget': './src/components/commerce-widget.tsx'
639
652
  },
653
+ effectApi: {
654
+ stem: 'recommendations',
655
+ prefix: '/commerce-api',
656
+ consumedBy: [
657
+ shellApp.id,
658
+ 'remote-commerce'
659
+ ]
660
+ },
640
661
  ownership: {
641
662
  team: 'commerce-experience',
642
663
  slack: '#commerce-experience',
@@ -666,6 +687,14 @@ const remoteApps = [
666
687
  './Route': './src/remote-entry.tsx',
667
688
  './Widget': './src/components/identity-widget.tsx'
668
689
  },
690
+ effectApi: {
691
+ stem: 'identity',
692
+ prefix: '/identity-api',
693
+ consumedBy: [
694
+ shellApp.id,
695
+ 'remote-identity'
696
+ ]
697
+ },
669
698
  ownership: {
670
699
  team: 'identity-platform',
671
700
  slack: '#identity-platform',
@@ -824,6 +853,89 @@ const sharedPackages = [
824
853
  description: 'Shared Effect API type placeholders for services and clients.'
825
854
  }
826
855
  ];
856
+ function createNeutralOwnership(id, tier = 'tier-2-microvertical') {
857
+ return {
858
+ team: 'super-app-platform',
859
+ slack: '#super-app-platform',
860
+ pagerDuty: 'pd-super-app-platform',
861
+ runbookRef: `runbooks/microverticals/${id}.md`,
862
+ adrRef: `docs/super-app-rfc-adr/microverticals.md#${id}`,
863
+ blastRadius: {
864
+ tier,
865
+ references: [
866
+ `docs/super-app-rfc-adr/blast-radius.md#${id}`
867
+ ]
868
+ }
869
+ };
870
+ }
871
+ function createRemoteDescriptor(name, kind, port) {
872
+ const domain = toKebabCase(name);
873
+ const id = `remote-${domain}`;
874
+ const displayPrefix = toPascalCase(domain).replace(/([a-z])([A-Z])/g, '$1 $2');
875
+ return {
876
+ id,
877
+ directory: `apps/remotes/${id}`,
878
+ packageSuffix: id,
879
+ displayName: `${displayPrefix} Remote`,
880
+ kind: 'horizontal-remote' === kind ? 'horizontal-remote' : 'vertical',
881
+ domain,
882
+ portEnv: `REMOTE_${toEnvSegment(domain)}_PORT`,
883
+ port,
884
+ mfName: `remote${toPascalCase(domain)}`,
885
+ exposes: {
886
+ './Route': './src/remote-entry.tsx',
887
+ './Widget': `./src/components/${domain}-widget.tsx`
888
+ },
889
+ ...'remote' === kind ? {
890
+ effectApi: {
891
+ stem: domain,
892
+ prefix: `/${domain}-api`,
893
+ consumedBy: [
894
+ shellApp.id,
895
+ id
896
+ ]
897
+ }
898
+ } : {},
899
+ ownership: createNeutralOwnership(id)
900
+ };
901
+ }
902
+ function createServiceDescriptor(name, port) {
903
+ const normalized = toKebabCase(name);
904
+ const suffix = normalized.endsWith('-effect') ? normalized : `service-${normalized.replace(/^service-/, '')}-effect`;
905
+ return {
906
+ id: suffix,
907
+ directory: `services/${suffix}`,
908
+ packageSuffix: suffix,
909
+ portEnv: `${toEnvSegment(suffix)}_PORT`,
910
+ port,
911
+ ownership: createNeutralOwnership(suffix, 'tier-2-effect-service')
912
+ };
913
+ }
914
+ function serviceApiPrefix(service) {
915
+ const name = service.id.replace(/^service-/, '').replace(/-effect$/, '');
916
+ return name.endsWith('-api') ? `/${name}` : `/${name}-api`;
917
+ }
918
+ function appHasEffectApi(app) {
919
+ return void 0 !== app.effectApi;
920
+ }
921
+ function effectApiPrefix(target) {
922
+ return target.effectApi?.prefix ?? serviceApiPrefix(target);
923
+ }
924
+ function effectApiStem(target) {
925
+ return target.effectApi?.stem ?? target.id.replace(/^service-/, '').replace(/-effect$/, '').replace(/-api$/, '');
926
+ }
927
+ function verticalEffectApps(remotes = remoteApps) {
928
+ return remotes.filter(appHasEffectApi);
929
+ }
930
+ function createSharedPackageDescriptor(name) {
931
+ const normalized = toKebabCase(name);
932
+ const id = normalized.startsWith('shared-') ? normalized : `shared-${normalized}`;
933
+ return {
934
+ id,
935
+ directory: `packages/${id}`,
936
+ description: `Shared ${normalized.replace(/^shared-/, '')} package placeholder.`
937
+ };
938
+ }
827
939
  function normalizePath(filePath) {
828
940
  return filePath.split(node_path.sep).join('/');
829
941
  }
@@ -844,6 +956,15 @@ function writeFile(targetDir, relativePath, content) {
844
956
  });
845
957
  node_fs.writeFileSync(filePath, content, 'utf-8');
846
958
  }
959
+ function writeFileReplacing(targetDir, relativePath, content) {
960
+ assertSafeRelativePath(relativePath);
961
+ const filePath = node_path.join(targetDir, relativePath);
962
+ ensureInsideRoot(targetDir, filePath);
963
+ node_fs.mkdirSync(node_path.dirname(filePath), {
964
+ recursive: true
965
+ });
966
+ node_fs.writeFileSync(filePath, content, 'utf-8');
967
+ }
847
968
  function writeJson(targetDir, relativePath, value) {
848
969
  writeFile(targetDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
849
970
  }
@@ -889,6 +1010,17 @@ function toPackageScope(packageName) {
889
1010
  const normalized = packageName.replace(/^@/, '').replace(/[\\/]+/g, '-').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '').replace(/-{2,}/g, '-');
890
1011
  return normalized || 'ultramodern-superapp';
891
1012
  }
1013
+ function toKebabCase(value) {
1014
+ const normalized = value.trim().replace(/([a-z0-9])([A-Z])/g, '$1-$2').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/[._]+/g, '-').toLowerCase().replace(/-+/g, '-').replace(/^-+|-+$/g, '');
1015
+ return normalized;
1016
+ }
1017
+ function toCamelCase(value) {
1018
+ const pascal = toPascalCase(value);
1019
+ return `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`;
1020
+ }
1021
+ function toEnvSegment(value) {
1022
+ return toKebabCase(value).replace(/-/g, '_').toUpperCase();
1023
+ }
892
1024
  function ultramodern_workspace_packageName(scope, suffix) {
893
1025
  return `@${scope}/${suffix}`;
894
1026
  }
@@ -919,31 +1051,44 @@ function modernPackageSpecifier(packageName, packageSource) {
919
1051
  if (!packageSource.aliasScope) return packageSource.modernPackageVersion;
920
1052
  return `npm:${modernAliasPackageName(packageName, packageSource)}@${packageSource.modernPackageVersion}`;
921
1053
  }
922
- function appDependencies(scope, packageSource) {
923
- return {
924
- '@modern-js/plugin-i18n': modernPackageSpecifier('@modern-js/plugin-i18n', packageSource),
1054
+ function appDependencies(scope, packageSource, app) {
1055
+ const dependencies = {
925
1056
  '@modern-js/plugin-tanstack': modernPackageSpecifier('@modern-js/plugin-tanstack', packageSource),
1057
+ '@modern-js/plugin-i18n': modernPackageSpecifier('@modern-js/plugin-i18n', packageSource),
926
1058
  '@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
927
1059
  '@module-federation/modern-js-v3': MODULE_FEDERATION_VERSION,
928
1060
  '@module-federation/runtime': MODULE_FEDERATION_VERSION,
929
1061
  '@tanstack/react-router': TANSTACK_ROUTER_VERSION,
930
- 'zephyr-modernjs-plugin': ZEPHYR_MODERNJS_PLUGIN_VERSION,
1062
+ i18next: I18NEXT_VERSION,
1063
+ 'node-fetch': '^3.3.2',
931
1064
  [ultramodern_workspace_packageName(scope, 'shared-contracts')]: WORKSPACE_PACKAGE_VERSION,
932
1065
  [ultramodern_workspace_packageName(scope, 'shared-design-tokens')]: WORKSPACE_PACKAGE_VERSION,
933
- i18next: I18NEXT_VERSION,
934
1066
  react: REACT_VERSION,
935
- 'react-dom': REACT_DOM_VERSION,
936
- 'react-i18next': REACT_I18NEXT_VERSION
1067
+ 'react-dom': REACT_DOM_VERSION
937
1068
  };
1069
+ if ('shell' === app.kind) {
1070
+ dependencies['@modern-js/plugin-bff'] = modernPackageSpecifier('@modern-js/plugin-bff', packageSource);
1071
+ for (const remote of verticalEffectApps())dependencies[ultramodern_workspace_packageName(scope, remote.packageSuffix)] = WORKSPACE_PACKAGE_VERSION;
1072
+ }
1073
+ if (appHasEffectApi(app)) dependencies['@modern-js/plugin-bff'] = modernPackageSpecifier('@modern-js/plugin-bff', packageSource);
1074
+ return dependencies;
938
1075
  }
939
- function appDevDependencies(packageSource) {
1076
+ function appDevDependencies(packageSource, enableTailwind) {
940
1077
  return {
941
1078
  '@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
942
1079
  '@effect/tsgo': EFFECT_TSGO_VERSION,
1080
+ ...enableTailwind ? {
1081
+ '@tailwindcss/postcss': `^${TAILWIND_POSTCSS_VERSION}`,
1082
+ postcss: '^8.5.6',
1083
+ tailwindcss: `^${TAILWIND_VERSION}`
1084
+ } : {},
943
1085
  "@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
944
1086
  '@types/node': '^20',
945
1087
  '@types/react': '^19.1.8',
946
- '@types/react-dom': '^19.1.6'
1088
+ '@types/react-dom': '^19.1.6',
1089
+ typescript: TYPESCRIPT_VERSION,
1090
+ 'zephyr-rspack-plugin': ZEPHYR_RSPACK_PLUGIN_VERSION,
1091
+ wrangler: WRANGLER_VERSION
947
1092
  };
948
1093
  }
949
1094
  function createRootPackageJson(scope, packageSource) {
@@ -952,32 +1097,32 @@ function createRootPackageJson(scope, packageSource) {
952
1097
  name: scope,
953
1098
  version: '0.1.0',
954
1099
  type: 'module',
955
- packageManager: 'pnpm@11.1.2',
1100
+ packageManager: 'pnpm@11.3.0',
956
1101
  scripts: {
957
1102
  dev: `pnpm --parallel --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} --filter ${ultramodern_workspace_packageName(scope, 'remote-commerce')} --filter ${ultramodern_workspace_packageName(scope, 'remote-identity')} --filter ${ultramodern_workspace_packageName(scope, 'remote-design-system')} dev`,
958
1103
  'dev:shell': `pnpm --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} dev`,
959
1104
  'dev:commerce': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-commerce')} dev`,
960
1105
  'dev:identity': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-identity')} dev`,
961
1106
  'dev:design-system': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-design-system')} dev`,
962
- 'dev:recommendations': `pnpm --filter ${ultramodern_workspace_packageName(scope, effectService.packageSuffix)} dev`,
963
- build: 'pnpm -r --filter ./apps/** --filter ./services/** build',
1107
+ build: 'pnpm -r --filter "./apps/remotes/**" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types',
964
1108
  format: 'oxfmt .',
965
1109
  'format:check': 'oxfmt --check .',
966
- 'i18n:check': "node ./scripts/check-i18n-strings.mjs",
967
1110
  lint: 'oxlint .',
968
1111
  'lint:fix': 'oxlint . --fix',
969
1112
  typecheck: `pnpm -r --filter "@${scope}/*" typecheck`,
1113
+ 'cloudflare:build': 'pnpm -r --filter "./apps/remotes/**" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types',
970
1114
  'skills:install': "node ./scripts/bootstrap-agent-skills.mjs",
971
1115
  'skills:check': "node ./scripts/bootstrap-agent-skills.mjs --check",
972
1116
  'agents:refs:install': "node ./scripts/setup-agent-reference-repos.mjs",
973
1117
  'agents:refs:check': "node ./scripts/setup-agent-reference-repos.mjs --check",
1118
+ 'ultramodern:assert-mf-types': "node ./scripts/assert-mf-types.mjs",
974
1119
  'ultramodern:check': "node ./scripts/validate-ultramodern-workspace.mjs",
975
- postinstall: "node ./scripts/setup-agent-reference-repos.mjs",
976
- check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm i18n:check && pnpm skills:check && pnpm ultramodern:check'
1120
+ postinstall: "node ./scripts/setup-agent-reference-repos.mjs && node ./scripts/bootstrap-agent-skills.mjs",
1121
+ check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm skills:check && pnpm ultramodern:check'
977
1122
  },
978
1123
  engines: {
979
1124
  node: '>=20',
980
- pnpm: '>=11.0.0'
1125
+ pnpm: '>=11.3.0 <11.4.0'
981
1126
  },
982
1127
  workspaces: [
983
1128
  'apps/*',
@@ -1000,10 +1145,25 @@ function createRootPackageJson(scope, packageSource) {
1000
1145
  "@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
1001
1146
  oxlint: OXLINT_VERSION,
1002
1147
  oxfmt: OXFMT_VERSION,
1003
- ultracite: ULTRACITE_VERSION
1148
+ ultracite: ULTRACITE_VERSION,
1149
+ wrangler: WRANGLER_VERSION,
1150
+ 'zephyr-agent': ZEPHYR_AGENT_VERSION
1004
1151
  }
1005
1152
  };
1006
1153
  }
1154
+ function remoteDependencyAlias(remote) {
1155
+ return toCamelCase(remote.domain ?? remote.id.replace(/^remote-/, ''));
1156
+ }
1157
+ function zephyrRemoteDependency(scope, remote) {
1158
+ return `${ultramodern_workspace_packageName(scope, remote.packageSuffix)}@workspace:*`;
1159
+ }
1160
+ function createZephyrDependencies(scope, app, remotes = remoteApps) {
1161
+ if ('shell' !== app.kind) return {};
1162
+ return Object.fromEntries(remotes.map((remote)=>[
1163
+ remoteDependencyAlias(remote),
1164
+ zephyrRemoteDependency(scope, remote)
1165
+ ]));
1166
+ }
1007
1167
  function createTsConfigBase() {
1008
1168
  return {
1009
1169
  compilerOptions: {
@@ -1058,17 +1218,22 @@ function createPackageTsConfig(packageDir, includeApi = false) {
1058
1218
  if (includeApi) include.push('api', 'shared');
1059
1219
  return {
1060
1220
  extends: `${relativeRootFor(packageDir)}/tsconfig.base.json`,
1061
- include
1221
+ include,
1222
+ exclude: [
1223
+ 'src/modern-tanstack'
1224
+ ]
1062
1225
  };
1063
1226
  }
1064
- function createAppPackage(scope, app, packageSource) {
1065
- return {
1227
+ function createAppPackage(scope, app, packageSource, enableTailwind) {
1228
+ const packageJson = {
1066
1229
  private: true,
1067
1230
  name: ultramodern_workspace_packageName(scope, app.packageSuffix),
1068
1231
  version: '0.1.0',
1069
1232
  scripts: {
1070
1233
  dev: 'modern dev',
1071
- build: 'modern build',
1234
+ build: app.exposes ? `modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : 'modern build',
1235
+ 'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy',
1236
+ 'cloudflare:preview': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy && wrangler dev --config .output/wrangler.json',
1072
1237
  serve: 'modern serve',
1073
1238
  typecheck: effectTsgoTypecheckCommand
1074
1239
  },
@@ -1076,16 +1241,25 @@ function createAppPackage(scope, app, packageSource) {
1076
1241
  preset: 'presetUltramodern',
1077
1242
  role: 'shell' === app.kind ? 'shell' : 'module-federation-remote',
1078
1243
  appId: app.id,
1079
- topology: `${relativeRootFor(app.directory)}/topology/reference-topology.json`
1244
+ topology: `${relativeRootFor(app.directory)}/topology/reference-topology.json`,
1245
+ ...appHasEffectApi(app) ? {
1246
+ apiRuntime: 'effect-bff'
1247
+ } : {}
1080
1248
  },
1081
- dependencies: appDependencies(scope, packageSource),
1082
- devDependencies: appDevDependencies(packageSource)
1249
+ 'zephyr:dependencies': createZephyrDependencies(scope, app),
1250
+ dependencies: appDependencies(scope, packageSource, app),
1251
+ devDependencies: appDevDependencies(packageSource, enableTailwind)
1083
1252
  };
1253
+ if (appHasEffectApi(app)) packageJson.exports = {
1254
+ './effect/client': `./src/effect/${app.effectApi.stem}-client.ts`,
1255
+ './shared/effect/api': './shared/effect/api.ts'
1256
+ };
1257
+ return packageJson;
1084
1258
  }
1085
- function createServicePackage(scope, packageSource) {
1259
+ function createServicePackage(scope, packageSource, enableTailwind, service = effectService) {
1086
1260
  return {
1087
1261
  private: true,
1088
- name: ultramodern_workspace_packageName(scope, effectService.packageSuffix),
1262
+ name: ultramodern_workspace_packageName(scope, service.packageSuffix),
1089
1263
  version: '0.1.0',
1090
1264
  scripts: {
1091
1265
  dev: 'modern dev',
@@ -1096,8 +1270,8 @@ function createServicePackage(scope, packageSource) {
1096
1270
  modernjs: {
1097
1271
  preset: 'presetUltramodern',
1098
1272
  role: 'effect-service',
1099
- appId: effectService.id,
1100
- topology: `${relativeRootFor(effectService.directory)}/topology/reference-topology.json`
1273
+ appId: service.id,
1274
+ topology: `${relativeRootFor(service.directory)}/topology/reference-topology.json`
1101
1275
  },
1102
1276
  dependencies: {
1103
1277
  '@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
@@ -1109,15 +1283,21 @@ function createServicePackage(scope, packageSource) {
1109
1283
  '@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
1110
1284
  '@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource),
1111
1285
  '@effect/tsgo': EFFECT_TSGO_VERSION,
1286
+ ...enableTailwind ? {
1287
+ '@tailwindcss/postcss': `^${TAILWIND_POSTCSS_VERSION}`,
1288
+ postcss: '^8.5.6',
1289
+ tailwindcss: `^${TAILWIND_VERSION}`
1290
+ } : {},
1112
1291
  "@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
1113
1292
  '@types/node': '^20',
1114
1293
  '@types/react': '^19.1.8',
1115
- '@types/react-dom': '^19.1.6'
1294
+ '@types/react-dom': '^19.1.6',
1295
+ typescript: TYPESCRIPT_VERSION
1116
1296
  }
1117
1297
  };
1118
1298
  }
1119
- function createSharedPackage(scope, id, description) {
1120
- return {
1299
+ function createSharedPackage(scope, id, description, packageSource) {
1300
+ const packageJson = {
1121
1301
  private: true,
1122
1302
  name: ultramodern_workspace_packageName(scope, id),
1123
1303
  version: '0.1.0',
@@ -1134,19 +1314,56 @@ function createSharedPackage(scope, id, description) {
1134
1314
  "@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION
1135
1315
  }
1136
1316
  };
1317
+ if ('shared-effect-api' === id) packageJson.dependencies = {
1318
+ '@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource)
1319
+ };
1320
+ return packageJson;
1137
1321
  }
1138
1322
  function createAppModernConfig(app) {
1323
+ const bffImport = appHasEffectApi(app) ? "import { bffPlugin } from '@modern-js/plugin-bff';\n" : '';
1324
+ const bffConfig = appHasEffectApi(app) ? ` bff: {
1325
+ effect: {
1326
+ openapi: {
1327
+ path: '/openapi.json',
1328
+ },
1329
+ },
1330
+ prefix: '${effectApiPrefix(app)}',
1331
+ runtimeFramework: 'effect',
1332
+ },
1333
+ ` : '';
1334
+ const bffPluginEntry = appHasEffectApi(app) ? ' bffPlugin(),\n' : '';
1139
1335
  return `// @effect-diagnostics processEnv:off
1140
- import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
1141
- import { i18nPlugin } from '@modern-js/plugin-i18n';
1336
+ import {
1337
+ appTools,
1338
+ defineConfig,
1339
+ presetUltramodern,
1340
+ } from '@modern-js/app-tools';
1341
+ ${bffImport}import { i18nPlugin } from '@modern-js/plugin-i18n';
1142
1342
  import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
1143
1343
  import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
1144
- import { withZephyr } from 'zephyr-modernjs-plugin';
1344
+ import { withZephyr as withZephyrRspack } from 'zephyr-rspack-plugin';
1345
+
1346
+ type ZephyrRspackConfig = Parameters<ReturnType<typeof withZephyrRspack>>[0];
1347
+
1348
+ const zephyrRspackPlugin = () => ({
1349
+ name: 'ultramodern-zephyr-rspack-plugin',
1350
+ pre: ['@modern-js/plugin-module-federation-config'],
1351
+ setup(api: {
1352
+ modifyRspackConfig: (
1353
+ handler: (
1354
+ config: ZephyrRspackConfig,
1355
+ ) => ZephyrRspackConfig | Promise<ZephyrRspackConfig>,
1356
+ ) => void;
1357
+ }) {
1358
+ api.modifyRspackConfig(config => withZephyrRspack()(config));
1359
+ },
1360
+ });
1145
1361
 
1146
1362
  const appId = '${app.id}';
1147
1363
  const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
1148
1364
  const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
1149
- const hasConfiguredSiteUrl = typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
1365
+ const hasConfiguredSiteUrl =
1366
+ typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
1150
1367
  const isProductionBuild =
1151
1368
  process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
1152
1369
 
@@ -1156,12 +1373,14 @@ if (isProductionBuild && !hasConfiguredSiteUrl) {
1156
1373
  );
1157
1374
  }
1158
1375
 
1159
- const siteUrl = hasConfiguredSiteUrl ? configuredSiteUrl : \`http://localhost:\${port}\`;
1376
+ const siteUrl = hasConfiguredSiteUrl
1377
+ ? configuredSiteUrl
1378
+ : \`http://localhost:\${port}\`;
1160
1379
 
1161
1380
  export default defineConfig(
1162
1381
  presetUltramodern(
1163
1382
  {
1164
- output: {
1383
+ ${bffConfig} output: {
1165
1384
  disableTsChecker: true,
1166
1385
  distPath: {
1167
1386
  html: './',
@@ -1173,24 +1392,44 @@ export default defineConfig(
1173
1392
  outputStructure: 'flat',
1174
1393
  },
1175
1394
  plugins: [
1176
- appTools({
1177
- bundler: 'rspack',
1178
- }),
1395
+ appTools(),
1396
+ tanstackRouterPlugin(),
1179
1397
  i18nPlugin({
1398
+ backend: {
1399
+ enabled: true,
1400
+ },
1401
+ reactI18next: false,
1180
1402
  localeDetection: {
1181
1403
  fallbackLanguage: 'en',
1182
1404
  languages: ['en', 'cs'],
1183
1405
  localePathRedirect: true,
1406
+ ignoreRedirectRoutes: [
1407
+ '/@mf-types',
1408
+ '/bundles',
1409
+ '${effectApiPrefix(app)}',
1410
+ '/locales',
1411
+ '/mf-manifest.json',
1412
+ '/mf-stats.json',
1413
+ '/remoteEntry.js',
1414
+ '/static',
1415
+ '/zephyr-manifest.json',
1416
+ ],
1184
1417
  },
1185
1418
  }),
1186
- tanstackRouterPlugin(),
1187
- moduleFederationPlugin(),
1188
- withZephyr(),
1419
+ ${bffPluginEntry} moduleFederationPlugin(),
1420
+ zephyrRspackPlugin(),
1189
1421
  ],
1422
+ deploy: {
1423
+ target: 'cloudflare',
1424
+ worker: {
1425
+ ssr: true,
1426
+ },
1427
+ },
1190
1428
  server: {
1191
1429
  port,
1430
+ publicDir: './locales',
1192
1431
  ssr: {
1193
- mode: 'string',
1432
+ mode: 'stream',
1194
1433
  moduleFederationAppSSR: true,
1195
1434
  },
1196
1435
  },
@@ -1224,11 +1463,6 @@ function createSharedModuleFederationConfig() {
1224
1463
  singleton: true,
1225
1464
  treeShaking: false,
1226
1465
  },
1227
- i18next: {
1228
- requiredVersion: dependencies.i18next,
1229
- singleton: true,
1230
- treeShaking: false,
1231
- },
1232
1466
  react: {
1233
1467
  requiredVersion: reactVersion,
1234
1468
  singleton: true,
@@ -1239,8 +1473,8 @@ function createSharedModuleFederationConfig() {
1239
1473
  singleton: true,
1240
1474
  treeShaking: false,
1241
1475
  },
1242
- 'react-i18next': {
1243
- requiredVersion: dependencies['react-i18next'],
1476
+ 'react-dom/client': {
1477
+ requiredVersion: reactDomVersion,
1244
1478
  singleton: true,
1245
1479
  treeShaking: false,
1246
1480
  },
@@ -1253,7 +1487,16 @@ function formatTsObjectLiteral(value) {
1253
1487
  ${entries.map(([key, entryValue])=>` '${key}': '${entryValue}',`).join('\n')}
1254
1488
  }`;
1255
1489
  }
1256
- function createShellModuleFederationConfig() {
1490
+ function createRemoteManifestEnv(remote) {
1491
+ return `REMOTE_${toEnvSegment(remote.domain ?? remote.id)}_MF_MANIFEST`;
1492
+ }
1493
+ function createShellModuleFederationConfig(remotes = remoteApps) {
1494
+ const remoteEntries = remotes.map((remote)=>{
1495
+ const key = remoteDependencyAlias(remote);
1496
+ return ` ${key}:
1497
+ process.env['${createRemoteManifestEnv(remote)}'] ??
1498
+ '${remote.mfName}@http://localhost:${remote.port}/mf-manifest.json',`;
1499
+ }).join('\n');
1257
1500
  return `// @effect-diagnostics nodeBuiltinImport:off processEnv:off
1258
1501
  import { createRequire } from 'node:module';
1259
1502
  import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
@@ -1265,24 +1508,48 @@ const reactVersion = (require('react/package.json') as { version: string }).vers
1265
1508
  const reactDomVersion = (require('react-dom/package.json') as { version: string }).version;
1266
1509
 
1267
1510
  export default createModuleFederationConfig({
1268
- dts: false,
1511
+ treeShakingSharedExcludePlugins: ['RspackModuleFederationPlugin'],
1512
+ dev: {
1513
+ disableDynamicRemoteTypeHints: true,
1514
+ },
1515
+ dts: {
1516
+ displayErrorInTerminal: true,
1517
+ generateTypes: {
1518
+ compilerInstance: '--package typescript -- tsc',
1519
+ },
1520
+ },
1269
1521
  filename: 'remoteEntry.js',
1270
1522
  name: '${shellApp.mfName}',
1271
1523
  remotes: {
1272
- commerce:
1273
- process.env['REMOTE_COMMERCE_MF_MANIFEST'] ??
1274
- 'remoteCommerce@http://localhost:3021/mf-manifest.json',
1275
- designSystem:
1276
- process.env['REMOTE_DESIGN_SYSTEM_MF_MANIFEST'] ??
1277
- 'remoteDesignSystem@http://localhost:3023/mf-manifest.json',
1278
- identity:
1279
- process.env['REMOTE_IDENTITY_MF_MANIFEST'] ??
1280
- 'remoteIdentity@http://localhost:3022/mf-manifest.json',
1524
+ ${remoteEntries}
1281
1525
  },
1282
1526
  ${createSharedModuleFederationConfig()},
1283
1527
  });
1284
1528
  `;
1285
1529
  }
1530
+ function createBuildMarker(scope, app) {
1531
+ return node_crypto.createHash('sha256').update(`${scope}:${app.packageSuffix}:${app.id}:0.1.0`).digest('hex').slice(0, 16);
1532
+ }
1533
+ function createUltramodernBuildModule(scope, app) {
1534
+ return `export const ultramodernVerticalIdentity = {
1535
+ appId: '${app.id}',
1536
+ packageName: '${ultramodern_workspace_packageName(scope, app.packageSuffix)}',
1537
+ version: '0.1.0',
1538
+ build: '${createBuildMarker(scope, app)}',
1539
+ deployProfile: 'cloudflare-ssr-mf-effect-v1',
1540
+ } as const;
1541
+
1542
+ export const ultramodernUiMarker = {
1543
+ ...ultramodernVerticalIdentity,
1544
+ surface: 'ui',
1545
+ } as const;
1546
+
1547
+ export const ultramodernApiMarker = {
1548
+ ...ultramodernVerticalIdentity,
1549
+ surface: 'effect-bff',
1550
+ } as const;
1551
+ `;
1552
+ }
1286
1553
  function createRemoteModuleFederationConfig(app) {
1287
1554
  const exposes = formatTsObjectLiteral(app.exposes ?? {});
1288
1555
  return `// @effect-diagnostics nodeBuiltinImport:off
@@ -1296,7 +1563,16 @@ const reactVersion = (require('react/package.json') as { version: string }).vers
1296
1563
  const reactDomVersion = (require('react-dom/package.json') as { version: string }).version;
1297
1564
 
1298
1565
  export default createModuleFederationConfig({
1299
- dts: false,
1566
+ treeShakingSharedExcludePlugins: ['RspackModuleFederationPlugin'],
1567
+ dev: {
1568
+ disableDynamicRemoteTypeHints: true,
1569
+ },
1570
+ dts: {
1571
+ displayErrorInTerminal: true,
1572
+ generateTypes: {
1573
+ compilerInstance: '--package typescript -- tsc',
1574
+ },
1575
+ },
1300
1576
  exposes: ${exposes},
1301
1577
  filename: 'remoteEntry.js',
1302
1578
  name: '${app.mfName}',
@@ -1304,13 +1580,16 @@ ${createSharedModuleFederationConfig()},
1304
1580
  });
1305
1581
  `;
1306
1582
  }
1307
- function createServiceModernConfig() {
1583
+ function remoteWidgetFile(app) {
1584
+ return `${app.domain ?? app.id.replace(/^remote-/, '')}-widget`;
1585
+ }
1586
+ function createServiceModernConfigFor(service = effectService) {
1308
1587
  return `// @effect-diagnostics processEnv:off
1309
1588
  import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
1310
1589
  import { bffPlugin } from '@modern-js/plugin-bff';
1311
1590
 
1312
- const appId = '${effectService.id}';
1313
- const port = Number(process.env['${effectService.portEnv}'] ?? ${effectService.port});
1591
+ const appId = '${service.id}';
1592
+ const port = Number(process.env['${service.portEnv}'] ?? ${service.port});
1314
1593
 
1315
1594
  export default defineConfig(
1316
1595
  presetUltramodern(
@@ -1321,7 +1600,7 @@ export default defineConfig(
1321
1600
  path: '/openapi.json',
1322
1601
  },
1323
1602
  },
1324
- prefix: '/recommendations-api',
1603
+ prefix: '${serviceApiPrefix(service)}',
1325
1604
  runtimeFramework: 'effect',
1326
1605
  },
1327
1606
  plugins: [appTools(), bffPlugin()],
@@ -1339,7 +1618,15 @@ export default defineConfig(
1339
1618
  );
1340
1619
  `;
1341
1620
  }
1342
- function createAppRuntimeConfig() {
1621
+ function createAppRuntimeConfig(app) {
1622
+ const resources = {
1623
+ cs: {
1624
+ translation: createAppLocaleMessages(app, 'cs')
1625
+ },
1626
+ en: {
1627
+ translation: createAppLocaleMessages(app, 'en')
1628
+ }
1629
+ };
1343
1630
  return `import { defineRuntimeConfig } from '@modern-js/runtime';
1344
1631
  import { createInstance } from 'i18next';
1345
1632
 
@@ -1355,6 +1642,7 @@ export default defineRuntimeConfig({
1355
1642
  escapeValue: false,
1356
1643
  },
1357
1644
  ns: ['translation'],
1645
+ resources: ${JSON.stringify(resources, null, 8).split('\n').join('\n ')},
1358
1646
  supportedLngs: ['en', 'cs'],
1359
1647
  },
1360
1648
  },
@@ -1364,109 +1652,126 @@ export default defineRuntimeConfig({
1364
1652
  });
1365
1653
  `;
1366
1654
  }
1367
- function createLocalizedHeadComponent(includeLocationSuffix = false) {
1368
- return `const fallbackLanguage = 'en';
1369
- const supportedLanguages = ['en', 'cs'] as const;
1370
- type SupportedLanguage = (typeof supportedLanguages)[number];
1655
+ function createAppStyles(enableTailwind) {
1656
+ return `${enableTailwind ? "@import 'tailwindcss';\n\n" : ''}:root {
1657
+ color: #10231c;
1658
+ background: #f6f8f7;
1659
+ font-family:
1660
+ Inter,
1661
+ ui-sans-serif,
1662
+ system-ui,
1663
+ -apple-system,
1664
+ BlinkMacSystemFont,
1665
+ "Segoe UI",
1666
+ sans-serif;
1667
+ }
1371
1668
 
1372
- const isSupportedLanguage = (value: string): value is SupportedLanguage =>
1373
- supportedLanguages.includes(value as SupportedLanguage);
1669
+ body {
1670
+ margin: 0;
1671
+ }
1374
1672
 
1375
- const stripLanguagePrefix = (pathname: string) => {
1376
- const segments = pathname.split('/').filter(Boolean);
1377
- if (segments.length > 0 && isSupportedLanguage(segments[0] ?? '')) {
1378
- segments.shift();
1379
- }
1380
- return \`/\${segments.join('/')}\`;
1381
- };
1673
+ main {
1674
+ min-height: 100vh;
1675
+ padding: 2rem;
1676
+ }
1677
+
1678
+ nav {
1679
+ display: flex;
1680
+ gap: 0.75rem;
1681
+ margin-bottom: 2rem;
1682
+ }
1382
1683
 
1383
- const localizedPath = (pathname: string, language: SupportedLanguage) => {
1384
- const pathWithoutLanguage = stripLanguagePrefix(pathname);
1385
- return pathWithoutLanguage === '/' ? \`/\${language}\` : \`/\${language}\${pathWithoutLanguage}\`;
1684
+ a {
1685
+ color: #166b4b;
1686
+ }
1687
+ `;
1688
+ }
1689
+ function createPostcssConfig() {
1690
+ return `export default {
1691
+ plugins: {
1692
+ '@tailwindcss/postcss': {},
1693
+ },
1386
1694
  };
1695
+ `;
1696
+ }
1697
+ function createTailwindConfig() {
1698
+ return `import type { Config } from 'tailwindcss';
1699
+
1700
+ export default {
1701
+ content: ['./src/**/*.{js,jsx,ts,tsx}'],
1702
+ } satisfies Config;
1703
+ `;
1704
+ }
1705
+ function createLocalizedHeadComponent() {
1706
+ return `const fallbackLanguage = 'en';
1707
+ const supportedLanguages = ['en', 'cs'] as const;
1708
+ type SupportedLanguage = (typeof supportedLanguages)[number];
1709
+
1710
+ const localizedPath = (language: SupportedLanguage) => \`/\${language}\`;
1387
1711
 
1388
1712
  const absoluteUrl = (pathname: string) => {
1389
1713
  const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
1390
1714
  return \`\${origin}\${pathname}\`;
1391
1715
  };
1392
- ${includeLocationSuffix ? `
1393
- const locationSuffix = (location: { hash?: unknown; search?: unknown; searchStr?: unknown }) => {
1394
- const { hash, search, searchStr } = location;
1395
- let locationSearch = '';
1396
- if (typeof searchStr === 'string') {
1397
- locationSearch = searchStr;
1398
- } else if (typeof search === 'string') {
1399
- locationSearch = search;
1400
- }
1401
- const locationHash = typeof hash === 'string' ? hash : '';
1402
- return \`\${locationSearch}\${locationHash}\`;
1403
- };
1404
- ` : ''}
1405
1716
  const LocalizedHead = () => {
1406
- const { language } = useModernI18n();
1407
- const location = useLocation();
1408
- const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1409
- const canonicalPath = localizedPath(location.pathname, currentLanguage);
1717
+ const canonicalPath = localizedPath(fallbackLanguage);
1410
1718
 
1411
1719
  return (
1412
- <Helmet>
1720
+ <>
1413
1721
  <link rel="canonical" href={absoluteUrl(canonicalPath)} />
1414
- {supportedLanguages.map((code) => (
1722
+ {supportedLanguages.map(code => (
1415
1723
  <link
1416
- href={absoluteUrl(localizedPath(location.pathname, code))}
1724
+ href={absoluteUrl(localizedPath(code))}
1417
1725
  hrefLang={code}
1418
1726
  key={code}
1419
1727
  rel="alternate"
1420
1728
  />
1421
1729
  ))}
1422
1730
  <link
1423
- href={absoluteUrl(localizedPath(location.pathname, fallbackLanguage))}
1731
+ href={absoluteUrl(localizedPath(fallbackLanguage))}
1424
1732
  hrefLang="x-default"
1425
1733
  rel="alternate"
1426
1734
  />
1427
- </Helmet>
1735
+ </>
1428
1736
  );
1429
1737
  };
1430
1738
  `;
1431
1739
  }
1432
1740
  function createShellPage() {
1433
- return `import { Helmet } from '@modern-js/runtime/head';
1434
- import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1435
- import { useLocation } from '@modern-js/plugin-tanstack/runtime';
1436
- import { useTranslation } from 'react-i18next';
1741
+ return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1742
+ import { ultramodernUiMarker } from '../../ultramodern-build';
1743
+ import '../index.css';
1744
+
1745
+ const languageCodes = ['en', 'cs'] as const;
1437
1746
 
1438
- const remotes = ['remote-commerce', 'remote-identity', 'remote-design-system'];
1747
+ const remoteKeys = ['commerce', 'identity', 'designSystem'] as const;
1439
1748
 
1440
- ${createLocalizedHeadComponent(true)}
1749
+ ${createLocalizedHeadComponent()}
1441
1750
  export default function ShellHome() {
1442
- const { t } = useTranslation();
1443
- const { language } = useModernI18n();
1444
- const location = useLocation();
1445
- const currentLanguage = isSupportedLanguage(language) ? language : fallbackLanguage;
1446
- const suffix = locationSuffix(location);
1447
- const languageOptions = supportedLanguages.map((code) => ({
1448
- code,
1449
- href: \`\${localizedPath(location.pathname, code)}\${suffix}\`,
1450
- label: t(\`language.\${code}\`),
1451
- }));
1751
+ const { i18nInstance, language } = useModernI18n();
1752
+ const t = i18nInstance.t.bind(i18nInstance);
1753
+
1452
1754
  return (
1453
1755
  <main>
1454
1756
  <LocalizedHead />
1455
- <nav aria-label={t('language.switcher')}>
1456
- {languageOptions.map((option) => (
1757
+ <nav aria-label={t('shell.language.switcher')}>
1758
+ {languageCodes.map(code => (
1457
1759
  <a
1458
- aria-current={currentLanguage === option.code ? 'page' : undefined}
1459
- href={option.href}
1460
- key={option.code}
1760
+ aria-current={language === code ? 'page' : undefined}
1761
+ href={\`/\${code}\`}
1762
+ key={code}
1461
1763
  >
1462
- {option.label}
1764
+ {t(\`shell.language.\${code}\`)}
1463
1765
  </a>
1464
1766
  ))}
1465
1767
  </nav>
1466
1768
  <h1>{t('shell.title')}</h1>
1467
- <p data-testid="ultramodern-preset">{t('shell.preset')}</p>
1769
+ <p data-testid="ultramodern-preset">presetUltramodern workspace</p>
1770
+ <p data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
1771
+ {ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
1772
+ </p>
1468
1773
  <ul>
1469
- {remotes.map((remote) => (
1774
+ {remoteKeys.map(remote => (
1470
1775
  <li key={remote}>{t(\`shell.remotes.\${remote}\`)}</li>
1471
1776
  ))}
1472
1777
  </ul>
@@ -1476,54 +1781,143 @@ export default function ShellHome() {
1476
1781
  `;
1477
1782
  }
1478
1783
  function createRemotePage(app) {
1479
- return `import { Helmet } from '@modern-js/runtime/head';
1480
- import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1481
- import { useLocation } from '@modern-js/plugin-tanstack/runtime';
1482
- import { useTranslation } from 'react-i18next';
1784
+ const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
1785
+ import { useEffect, useState } from 'react';
1786
+ import { ultramodernUiMarker } from '../../ultramodern-build';
1787
+ ` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
1788
+ const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
1789
+
1790
+ useEffect(() => {
1791
+ void fetch('${effectApiPrefix(app)}/effect/${effectApiStem(app)}?limit=1', {
1792
+ headers: {
1793
+ accept: 'application/json',
1794
+ },
1795
+ })
1796
+ .then(response => {
1797
+ if (!response.ok) {
1798
+ throw new Error(\`Effect BFF request failed: \${response.status}\`);
1799
+ }
1800
+
1801
+ return response.json() as Promise<{ items?: Array<{ title?: string }> }>;
1802
+ })
1803
+ .then(data => {
1804
+ setEffectApiStatus(data.items[0]?.title ?? 'empty');
1805
+ })
1806
+ .catch(() => {
1807
+ setEffectApiStatus('unavailable');
1808
+ });
1809
+ }, []);
1810
+
1811
+ ` : '';
1812
+ const effectBffMarkup = appHasEffectApi(app) ? ` <p data-testid="effect-bff-status">{effectApiStatus}</p>
1813
+ ` : '';
1814
+ return `${effectBffImport}import '../index.css';
1483
1815
 
1484
1816
  ${createLocalizedHeadComponent()}
1485
1817
  export default function ${toPascalCase(app.id)}Home() {
1486
- const { t } = useTranslation();
1487
-
1488
- return (
1818
+ const { i18nInstance, language } = useModernI18n();
1819
+ const t = i18nInstance.t.bind(i18nInstance);
1820
+ ${effectBffState} return (
1489
1821
  <main>
1490
1822
  <LocalizedHead />
1491
- <h1>{t('remote.title')}</h1>
1492
- <p data-mf-role="${app.kind}">{t('remote.domain')}</p>
1493
- </main>
1823
+ <nav aria-label={t('${app.domain}.language.switcher')}>
1824
+ {supportedLanguages.map(code => (
1825
+ <a
1826
+ aria-current={language === code ? 'page' : undefined}
1827
+ href={\`/\${code}\`}
1828
+ key={code}
1829
+ >
1830
+ {t(\`${app.domain}.language.\${code}\`)}
1831
+ </a>
1832
+ ))}
1833
+ </nav>
1834
+ <h1>{t('${app.domain}.title')}</h1>
1835
+ <p data-mf-role="${app.kind}">{t('${app.domain}.role')}</p>
1836
+ <p data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
1837
+ {ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
1838
+ </p>
1839
+ ${effectBffMarkup} </main>
1494
1840
  );
1495
1841
  }
1496
1842
  `;
1497
1843
  }
1498
1844
  function createLayout(appId) {
1499
- return `import type { ReactNode } from 'react';
1845
+ return `import { Outlet } from '@modern-js/plugin-tanstack/runtime';
1846
+ import './index.css';
1500
1847
 
1501
- export default function Layout({ children }: { children: ReactNode }) {
1502
- return <div data-app-id="${appId}">{children}</div>;
1848
+ export default function Layout() {
1849
+ return (
1850
+ <div data-app-id="${appId}">
1851
+ <Outlet />
1852
+ </div>
1853
+ );
1503
1854
  }
1504
1855
  `;
1505
1856
  }
1506
1857
  function createRemoteEntry(app) {
1507
- const componentFile = 'remote-identity' === app.id ? 'identity-widget' : 'commerce-widget';
1508
- return `export { default } from './components/${componentFile}';
1858
+ return `export { default } from './components/${remoteWidgetFile(app)}';
1509
1859
  `;
1510
1860
  }
1511
1861
  function createRemoteWidget(app) {
1512
- const componentName = 'remote-identity' === app.id ? 'IdentityWidget' : 'CommerceWidget';
1513
- return `import { useTranslation } from 'react-i18next';
1514
-
1515
- export default function ${componentName}() {
1516
- const { t } = useTranslation();
1517
-
1862
+ const componentName = `${toPascalCase(app.domain ?? app.id)}Widget`;
1863
+ const body = 'vertical' === app.kind ? `Owns the ${app.domain} vertical route surface.` : 'Provides shared UI primitives for the workspace.';
1864
+ return `export default function ${componentName}() {
1518
1865
  return (
1519
1866
  <section data-mf-remote="${app.id}">
1520
- <h2>{t('remote.widget.title')}</h2>
1521
- <p>{t('remote.widget.body')}</p>
1867
+ <h2>${app.displayName}</h2>
1868
+ <p>${body}</p>
1522
1869
  </section>
1523
1870
  );
1524
1871
  }
1525
1872
  `;
1526
1873
  }
1874
+ function createAppLocaleMessages(app, language) {
1875
+ const czechLabels = {
1876
+ commerce: {
1877
+ role: 'obchod',
1878
+ title: 'Obchodni remote'
1879
+ },
1880
+ 'design-system': {
1881
+ role: 'design system',
1882
+ title: 'Design system remote'
1883
+ },
1884
+ identity: {
1885
+ role: 'identita',
1886
+ title: 'Identitni remote'
1887
+ }
1888
+ };
1889
+ if ('shell' === app.kind) return {
1890
+ shell: {
1891
+ language: {
1892
+ cs: 'en' === language ? 'Czech' : 'Cestina',
1893
+ en: 'en' === language ? 'English' : 'Anglictina',
1894
+ switcher: 'en' === language ? 'Language' : 'Jazyk'
1895
+ },
1896
+ remotes: {
1897
+ commerce: 'en' === language ? 'Commerce Remote' : 'Obchodni remote',
1898
+ designSystem: 'en' === language ? 'Design System Remote' : 'Design system remote',
1899
+ identity: 'en' === language ? 'Identity Remote' : 'Identitni remote'
1900
+ },
1901
+ title: 'en' === language ? 'UltraModern SuperApp Shell' : 'UltraModern SuperApp shell'
1902
+ }
1903
+ };
1904
+ const domain = app.domain ?? app.id;
1905
+ const czechLabel = czechLabels[domain] ?? {
1906
+ role: domain,
1907
+ title: `${app.displayName} CZ`
1908
+ };
1909
+ return {
1910
+ [domain]: {
1911
+ language: {
1912
+ cs: 'en' === language ? 'Czech' : 'Cestina',
1913
+ en: 'en' === language ? 'English' : 'Anglictina',
1914
+ switcher: 'en' === language ? 'Language' : 'Jazyk'
1915
+ },
1916
+ role: 'en' === language ? app.domain ?? app.kind : czechLabel.role,
1917
+ title: 'en' === language ? app.displayName : czechLabel.title
1918
+ }
1919
+ };
1920
+ }
1527
1921
  function createDesignButton() {
1528
1922
  return `import { designTokens } from '../tokens';
1529
1923
 
@@ -1554,124 +1948,373 @@ function createDesignTokens() {
1554
1948
  } as const;
1555
1949
  `;
1556
1950
  }
1557
- function createEnglishTranslations(app) {
1558
- if ('shell' === app.kind) return {
1559
- language: {
1560
- cs: 'Czech',
1561
- en: 'English',
1562
- switcher: 'Language'
1563
- },
1564
- shell: {
1565
- preset: 'presetUltramodern workspace',
1566
- remotes: {
1567
- 'remote-commerce': 'Commerce Remote',
1568
- 'remote-design-system': 'Design System Remote',
1569
- 'remote-identity': 'Identity Remote'
1570
- },
1571
- title: 'UltraModern SuperApp Shell'
1572
- }
1573
- };
1574
- return {
1575
- remote: {
1576
- domain: app.domain ?? app.kind,
1577
- title: app.displayName,
1578
- widget: {
1579
- body: 'vertical' === app.kind ? `Owns the ${app.domain} vertical route surface.` : 'Provides shared UI primitives for the workspace.',
1580
- title: app.displayName
1581
- }
1582
- }
1583
- };
1951
+ function serviceEffectApiExport(service = effectService) {
1952
+ return `${toCamelCase(effectApiStem(service))}EffectApi`;
1584
1953
  }
1585
- function createCzechTranslations(app) {
1586
- if ('shell' === app.kind) return {
1587
- language: {
1588
- cs: 'Cestina',
1589
- en: 'Anglictina',
1590
- switcher: 'Jazyk'
1591
- },
1592
- shell: {
1593
- preset: 'presetUltramodern workspace',
1594
- remotes: {
1595
- 'remote-commerce': 'Commerce remote',
1596
- 'remote-design-system': 'Design system remote',
1597
- 'remote-identity': 'Identity remote'
1598
- },
1599
- title: 'UltraModern SuperApp shell'
1600
- }
1601
- };
1602
- return {
1603
- remote: {
1604
- domain: app.domain ?? app.kind,
1605
- title: app.displayName,
1606
- widget: {
1607
- body: 'vertical' === app.kind ? `Vlastni ${app.domain} vertical route surface.` : 'Poskytuje sdilene UI prvky pro workspace.',
1608
- title: app.displayName
1609
- }
1610
- }
1611
- };
1954
+ function serviceEffectGroupName(service = effectService) {
1955
+ return toCamelCase(effectApiStem(service));
1956
+ }
1957
+ function serviceEffectApiName(service = effectService) {
1958
+ return `${toPascalCase(effectApiStem(service))}EffectApi`;
1959
+ }
1960
+ function serviceEffectSchemaExport(service = effectService) {
1961
+ return `${toCamelCase(effectApiStem(service))}ItemSchema`;
1962
+ }
1963
+ function serviceEffectErrorStem(service = effectService) {
1964
+ const stem = effectApiStem(service);
1965
+ return 'recommendations' === stem ? 'recommendation' : stem;
1966
+ }
1967
+ function serviceEffectCreatePayloadSchemaExport(service = effectService) {
1968
+ return `${toCamelCase(effectApiStem(service))}CreatePayloadSchema`;
1969
+ }
1970
+ function serviceEffectNotFoundErrorExport(service = effectService) {
1971
+ return `${toPascalCase(serviceEffectErrorStem(service))}NotFound`;
1972
+ }
1973
+ function serviceEffectNotFoundSchemaExport(service = effectService) {
1974
+ return `${toCamelCase(serviceEffectErrorStem(service))}NotFoundSchema`;
1612
1975
  }
1613
- function createEffectSharedApi() {
1976
+ function createEffectSharedApiImports() {
1614
1977
  return `import {
1615
1978
  HttpApi,
1616
1979
  HttpApiEndpoint,
1617
1980
  HttpApiGroup,
1981
+ HttpApiSchema,
1618
1982
  Schema,
1619
1983
  } from '@modern-js/plugin-bff/effect-client';
1620
-
1621
- const recommendationSchema = Schema.Struct({
1984
+ `;
1985
+ }
1986
+ function createEffectSharedApiContract(service = effectService) {
1987
+ const schemaExport = serviceEffectSchemaExport(service);
1988
+ const createPayloadSchemaExport = serviceEffectCreatePayloadSchemaExport(service);
1989
+ const notFoundErrorExport = serviceEffectNotFoundErrorExport(service);
1990
+ const notFoundSchemaExport = serviceEffectNotFoundSchemaExport(service);
1991
+ const apiExport = serviceEffectApiExport(service);
1992
+ const apiName = serviceEffectApiName(service);
1993
+ const groupName = serviceEffectGroupName(service);
1994
+ const stem = effectApiStem(service);
1995
+ const servicePrefix = effectApiPrefix(service);
1996
+ return `export const ${schemaExport} = Schema.Struct({
1622
1997
  id: Schema.String,
1998
+ marker: Schema.Struct({
1999
+ appId: Schema.String,
2000
+ packageName: Schema.String,
2001
+ version: Schema.String,
2002
+ build: Schema.String,
2003
+ deployProfile: Schema.String,
2004
+ surface: Schema.String,
2005
+ }),
1623
2006
  title: Schema.String,
1624
2007
  });
1625
2008
 
1626
- export const recommendationsEffectApi = HttpApi.make('RecommendationsEffectApi').add(
1627
- HttpApiGroup.make('recommendations').add(
1628
- HttpApiEndpoint.get('list', '/effect/recommendations', {
1629
- success: Schema.Struct({
1630
- items: Schema.Array(recommendationSchema),
2009
+ export const ${createPayloadSchemaExport} = Schema.Struct({
2010
+ title: Schema.String,
2011
+ });
2012
+
2013
+ export class ${notFoundErrorExport} extends Schema.TaggedErrorClass<${notFoundErrorExport}>()(
2014
+ '${notFoundErrorExport}',
2015
+ {
2016
+ id: Schema.String,
2017
+ },
2018
+ ) {}
2019
+
2020
+ export const ${notFoundSchemaExport} = ${notFoundErrorExport}.pipe(
2021
+ HttpApiSchema.status(404),
2022
+ );
2023
+
2024
+ export type OperationContext = {
2025
+ operationId: string;
2026
+ routePath: string;
2027
+ method: string;
2028
+ source: string;
2029
+ traceId?: string;
2030
+ };
2031
+
2032
+ export const ${apiExport} = HttpApi.make('${apiName}').add(
2033
+ HttpApiGroup.make('${groupName}')
2034
+ .add(
2035
+ HttpApiEndpoint.get('list', '/effect/${stem}', {
2036
+ query: {
2037
+ limit: Schema.optional(Schema.NumberFromString),
2038
+ },
2039
+ success: Schema.Struct({
2040
+ items: Schema.Array(${schemaExport}),
2041
+ }),
1631
2042
  }),
1632
- }),
1633
- ),
2043
+ )
2044
+ .add(
2045
+ HttpApiEndpoint.get('get', '/effect/${stem}/:id', {
2046
+ params: {
2047
+ id: Schema.String,
2048
+ },
2049
+ success: ${schemaExport},
2050
+ error: ${notFoundSchemaExport},
2051
+ }),
2052
+ )
2053
+ .add(
2054
+ HttpApiEndpoint.post('create', '/effect/${stem}', {
2055
+ payload: ${createPayloadSchemaExport},
2056
+ success: Schema.Struct({
2057
+ item: ${schemaExport},
2058
+ }),
2059
+ }),
2060
+ ),
1634
2061
  );
2062
+
2063
+ export const ${groupName}OperationContexts = {
2064
+ list: {
2065
+ operationId: '${apiName}:${groupName}:list',
2066
+ routePath: '/effect/${stem}',
2067
+ method: 'GET',
2068
+ source: 'generated-client',
2069
+ },
2070
+ get: {
2071
+ operationId: '${apiName}:${groupName}:get',
2072
+ routePath: '/effect/${stem}/:id',
2073
+ method: 'GET',
2074
+ source: 'generated-client',
2075
+ },
2076
+ create: {
2077
+ operationId: '${apiName}:${groupName}:create',
2078
+ routePath: '/effect/${stem}',
2079
+ method: 'POST',
2080
+ source: 'generated-client',
2081
+ },
2082
+ } satisfies Record<string, OperationContext>;
2083
+
2084
+ export const ${groupName}ApiContract = {
2085
+ basePath: '${servicePrefix}/effect/${stem}',
2086
+ ownerId: '${service.id}',
2087
+ servicePrefix: '${servicePrefix}',
2088
+ } as const;
2089
+ `;
2090
+ }
2091
+ function createEffectSharedApi(service) {
2092
+ if (service) return `${createEffectSharedApiImports()}
2093
+ ${createEffectSharedApiContract(service)}`;
2094
+ return `export const sharedEffectApiPackage = {
2095
+ scope: 'external-effect-service-contracts',
2096
+ } as const;
1635
2097
  `;
1636
2098
  }
1637
- function createEffectServiceEntry() {
2099
+ function createEffectServiceEntry(scope, service = effectService, contractImportPath = ultramodern_workspace_packageName(scope, 'shared-effect-api')) {
2100
+ const apiExport = serviceEffectApiExport(service);
2101
+ const groupName = serviceEffectGroupName(service);
2102
+ const notFoundErrorExport = serviceEffectNotFoundErrorExport(service);
2103
+ const stem = effectApiStem(service);
1638
2104
  return `import {
1639
2105
  defineEffectBff,
1640
2106
  Effect,
1641
2107
  HttpApiBuilder,
1642
2108
  Layer,
1643
- } from '@modern-js/plugin-bff/effect-server';
1644
- import { recommendationsEffectApi } from '../../shared/effect/api';
2109
+ } from '@modern-js/plugin-bff/effect-edge';
2110
+ import { ultramodernApiMarker } from '../../src/ultramodern-build';
2111
+ import {
2112
+ ${apiExport},
2113
+ ${groupName}OperationContexts,
2114
+ ${notFoundErrorExport},
2115
+ type OperationContext,
2116
+ } from '${contractImportPath}';
2117
+
2118
+ const ${groupName}Items = [
2119
+ {
2120
+ id: 'starter-${stem}',
2121
+ marker: ultramodernApiMarker,
2122
+ title: 'Wire a real ${stem} source here',
2123
+ },
2124
+ ];
2125
+
2126
+ const operationAttributes = (operationContext: OperationContext) => {
2127
+ return {
2128
+ 'modernjs.operation.id': operationContext.operationId,
2129
+ 'modernjs.operation.method': operationContext.method,
2130
+ 'modernjs.operation.route': operationContext.routePath,
2131
+ 'modernjs.operation.source': operationContext.source,
2132
+ ...(typeof operationContext.traceId === 'string'
2133
+ ? { 'modernjs.trace.id': operationContext.traceId }
2134
+ : {}),
2135
+ };
2136
+ };
1645
2137
 
1646
- const recommendationsLayer = HttpApiBuilder.group(
1647
- recommendationsEffectApi,
1648
- 'recommendations',
2138
+ const ${groupName}Layer = HttpApiBuilder.group(
2139
+ ${apiExport},
2140
+ '${groupName}',
1649
2141
  (handlers) =>
1650
- handlers.handle('list', () =>
1651
- Effect.succeed({
1652
- items: [
1653
- {
1654
- id: 'starter-recommendation',
1655
- title: 'Wire a real recommendation source here',
2142
+ handlers
2143
+ .handle('list', ({ query }) =>
2144
+ Effect.succeed({
2145
+ items:
2146
+ typeof query.limit === 'number'
2147
+ ? ${groupName}Items.slice(0, query.limit)
2148
+ : ${groupName}Items,
2149
+ }).pipe(
2150
+ Effect.withSpan('ultramodern.effect.${groupName}.list', {
2151
+ attributes: operationAttributes(${groupName}OperationContexts.list),
2152
+ kind: 'server',
2153
+ }),
2154
+ ),
2155
+ )
2156
+ .handle('get', ({ params }) => {
2157
+ const item = ${groupName}Items.find(item => item.id === params.id);
2158
+ return (item !== undefined
2159
+ ? Effect.succeed(item)
2160
+ : Effect.fail(new ${notFoundErrorExport}({ id: params.id }))).pipe(
2161
+ Effect.withSpan('ultramodern.effect.${groupName}.get', {
2162
+ attributes: operationAttributes(${groupName}OperationContexts.get),
2163
+ kind: 'server',
2164
+ }),
2165
+ );
2166
+ })
2167
+ .handle('create', ({ payload }) =>
2168
+ Effect.succeed({
2169
+ item: {
2170
+ id: \`generated-${stem}-\${payload.title
2171
+ .toLowerCase()
2172
+ .replaceAll(/[^a-z0-9]+/g, '-')
2173
+ .replaceAll(/^-|-$/g, '')}\`,
2174
+ marker: ultramodernApiMarker,
2175
+ title: payload.title,
1656
2176
  },
1657
- ],
1658
- }),
1659
- ),
2177
+ }).pipe(
2178
+ Effect.withSpan('ultramodern.effect.${groupName}.create', {
2179
+ attributes: operationAttributes(${groupName}OperationContexts.create),
2180
+ kind: 'server',
2181
+ }),
2182
+ ),
2183
+ ),
1660
2184
  );
1661
2185
 
1662
- const layer = HttpApiBuilder.layer(recommendationsEffectApi).pipe(
1663
- Layer.provide(recommendationsLayer),
2186
+ const layer = HttpApiBuilder.layer(${apiExport}).pipe(
2187
+ Layer.provide(${groupName}Layer),
1664
2188
  );
1665
2189
 
1666
2190
  export default defineEffectBff({
1667
- api: recommendationsEffectApi,
2191
+ api: ${apiExport},
1668
2192
  layer,
1669
2193
  });
1670
2194
  `;
1671
2195
  }
2196
+ function createEffectClient(service, contractImportPath) {
2197
+ const apiExport = serviceEffectApiExport(service);
2198
+ const contractExport = serviceEffectGroupName(service);
2199
+ const stem = effectApiStem(service);
2200
+ const groupName = serviceEffectGroupName(service);
2201
+ const singular = serviceEffectErrorStem(service);
2202
+ const clientOptionsName = `${toPascalCase(stem)}ClientOptions`;
2203
+ const createClientName = `create${toPascalCase(stem)}Client`;
2204
+ const listName = `list${toPascalCase(stem)}`;
2205
+ const getName = `get${toPascalCase(singular)}`;
2206
+ const createName = `create${toPascalCase(singular)}`;
2207
+ return `import {
2208
+ makeEffectHttpApiClient,
2209
+ runEffectRequest,
2210
+ } from '@modern-js/plugin-bff/effect-client';
2211
+ import {
2212
+ ${contractExport}ApiContract,
2213
+ ${apiExport},
2214
+ ${groupName}OperationContexts,
2215
+ type OperationContext,
2216
+ } from '${contractImportPath}';
2217
+
2218
+ export type ${clientOptionsName} = {
2219
+ baseUrl?: string | URL;
2220
+ locale?: string;
2221
+ operationContext?: OperationContext;
2222
+ traceparent?: string;
2223
+ };
2224
+
2225
+ export function ${createClientName}(
2226
+ options: ${clientOptionsName} = {},
2227
+ ) {
2228
+ return makeEffectHttpApiClient(${apiExport}, {
2229
+ baseUrl: options.baseUrl ?? ${contractExport}ApiContract.servicePrefix,
2230
+ });
2231
+ }
2232
+
2233
+ export function ${listName}(
2234
+ options: ${clientOptionsName} & { limit?: number } = {},
2235
+ ) {
2236
+ return runEffectRequest(
2237
+ ${createClientName}({
2238
+ ...options,
2239
+ operationContext:
2240
+ options.operationContext ?? ${groupName}OperationContexts.list,
2241
+ }),
2242
+ ).then(client =>
2243
+ runEffectRequest(
2244
+ client.${groupName}.list({ query: { limit: options.limit } }),
2245
+ ),
2246
+ );
2247
+ }
2248
+
2249
+ export function ${getName}(
2250
+ id: string,
2251
+ options: ${clientOptionsName} = {},
2252
+ ) {
2253
+ return runEffectRequest(
2254
+ ${createClientName}({
2255
+ ...options,
2256
+ operationContext:
2257
+ options.operationContext ?? ${groupName}OperationContexts.get,
2258
+ }),
2259
+ ).then(client =>
2260
+ runEffectRequest(client.${groupName}.get({ params: { id } })),
2261
+ );
2262
+ }
2263
+
2264
+ export function ${createName}(
2265
+ title: string,
2266
+ options: ${clientOptionsName} = {},
2267
+ ) {
2268
+ return runEffectRequest(
2269
+ ${createClientName}({
2270
+ ...options,
2271
+ operationContext:
2272
+ options.operationContext ?? ${groupName}OperationContexts.create,
2273
+ }),
2274
+ ).then(client =>
2275
+ runEffectRequest(
2276
+ client.${groupName}.create({ payload: { title } }),
2277
+ ),
2278
+ );
2279
+ }
2280
+ `;
2281
+ }
2282
+ function createShellEffectClient(scope) {
2283
+ return `export {
2284
+ createRecommendation,
2285
+ createRecommendationsClient,
2286
+ getRecommendation,
2287
+ listRecommendations,
2288
+ type RecommendationsClientOptions,
2289
+ } from '${ultramodern_workspace_packageName(scope, 'remote-commerce')}/effect/client';
2290
+ `;
2291
+ }
1672
2292
  function toPascalCase(value) {
1673
2293
  return value.split(/[-_]+/).filter(Boolean).map((part)=>`${part.charAt(0).toUpperCase()}${part.slice(1)}`).join('');
1674
2294
  }
2295
+ function effectApiTopologyMetadata(app) {
2296
+ if (!appHasEffectApi(app)) return;
2297
+ return {
2298
+ effect: {
2299
+ runtime: 'effect',
2300
+ bff: {
2301
+ prefix: app.effectApi.prefix,
2302
+ openapi: '/openapi.json'
2303
+ },
2304
+ contract: {
2305
+ export: './shared/effect/api',
2306
+ path: `${app.directory}/shared/effect/api.ts`
2307
+ },
2308
+ client: {
2309
+ export: './effect/client',
2310
+ path: `${app.directory}/src/effect/${app.effectApi.stem}-client.ts`
2311
+ },
2312
+ serverEntry: `${app.directory}/api/effect/index.ts`,
2313
+ basePath: `${app.effectApi.prefix}/effect/${app.effectApi.stem}`,
2314
+ consumedBy: app.effectApi.consumedBy
2315
+ }
2316
+ };
2317
+ }
1675
2318
  function createTopology(scope) {
1676
2319
  return {
1677
2320
  schemaVersion: 1,
@@ -1711,25 +2354,12 @@ function createTopology(scope) {
1711
2354
  fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
1712
2355
  sharedContractVersion: 'mf-ssr-contract-v1'
1713
2356
  },
2357
+ ...effectApiTopologyMetadata(remote) ? {
2358
+ api: effectApiTopologyMetadata(remote)
2359
+ } : {},
1714
2360
  ownership: remote.ownership
1715
2361
  })),
1716
- effectServices: [
1717
- {
1718
- id: effectService.id,
1719
- kind: 'effect-service',
1720
- runtime: 'effect',
1721
- package: ultramodern_workspace_packageName(scope, effectService.packageSuffix),
1722
- consumedBy: [
1723
- shellApp.id,
1724
- 'remote-commerce'
1725
- ],
1726
- bff: {
1727
- prefix: '/recommendations-api',
1728
- openapi: '/openapi.json'
1729
- },
1730
- ownership: effectService.ownership
1731
- }
1732
- ],
2362
+ effectServices: [],
1733
2363
  sharedPackages: sharedPackages.map((sharedPackage)=>({
1734
2364
  id: sharedPackage.id,
1735
2365
  package: ultramodern_workspace_packageName(scope, sharedPackage.id),
@@ -1751,12 +2381,6 @@ function createOwnership(scope) {
1751
2381
  owners: [
1752
2382
  shellApp,
1753
2383
  ...remoteApps,
1754
- {
1755
- id: effectService.id,
1756
- packageSuffix: effectService.packageSuffix,
1757
- directory: effectService.directory,
1758
- ownership: effectService.ownership
1759
- },
1760
2384
  ...sharedPackages.map((sharedPackage)=>({
1761
2385
  id: sharedPackage.id,
1762
2386
  packageSuffix: sharedPackage.id,
@@ -1794,19 +2418,15 @@ function createDevelopmentOverlay() {
1794
2418
  ].map((app)=>[
1795
2419
  app.id,
1796
2420
  app.port
1797
- ]).concat([
1798
- [
1799
- effectService.id,
1800
- effectService.port
1801
- ]
1802
- ])),
2421
+ ])),
1803
2422
  manifests: Object.fromEntries(remoteApps.map((remote)=>[
1804
2423
  remote.id,
1805
2424
  `http://localhost:${remote.port}/mf-manifest.json`
1806
2425
  ])),
1807
- services: {
1808
- [effectService.id]: `http://localhost:${effectService.port}/recommendations-api`
1809
- }
2426
+ apis: Object.fromEntries(verticalEffectApps().map((app)=>[
2427
+ app.id,
2428
+ `http://localhost:${app.port}${effectApiPrefix(app)}`
2429
+ ]))
1810
2430
  };
1811
2431
  }
1812
2432
  function createPackageSourceMetadata(scope, packageSource) {
@@ -1836,6 +2456,185 @@ function createPackageSourceMetadata(scope, packageSource) {
1836
2456
  }
1837
2457
  };
1838
2458
  }
2459
+ function createEffectOperationContract(target) {
2460
+ const stem = effectApiStem(target);
2461
+ return {
2462
+ group: serviceEffectGroupName(target),
2463
+ notFound: serviceEffectNotFoundErrorExport(target),
2464
+ operations: {
2465
+ list: {
2466
+ method: 'GET',
2467
+ path: `/effect/${stem}`,
2468
+ source: 'generated-client'
2469
+ },
2470
+ get: {
2471
+ method: 'GET',
2472
+ path: `/effect/${stem}/:id`,
2473
+ source: 'generated-client'
2474
+ },
2475
+ create: {
2476
+ method: 'POST',
2477
+ path: `/effect/${stem}`,
2478
+ source: 'generated-client'
2479
+ }
2480
+ }
2481
+ };
2482
+ }
2483
+ function createAppConfigContract(app) {
2484
+ return {
2485
+ preset: 'presetUltramodern',
2486
+ plugins: [
2487
+ 'appTools',
2488
+ 'tanstackRouterPlugin',
2489
+ 'i18nPlugin',
2490
+ ...appHasEffectApi(app) ? [
2491
+ 'bffPlugin'
2492
+ ] : [],
2493
+ 'moduleFederationPlugin',
2494
+ 'zephyrRspackPlugin'
2495
+ ],
2496
+ output: {
2497
+ disableTsChecker: true,
2498
+ distPath: {
2499
+ html: './'
2500
+ },
2501
+ polyfill: 'off',
2502
+ splitRouteChunks: false
2503
+ },
2504
+ html: {
2505
+ outputStructure: 'flat'
2506
+ },
2507
+ source: {
2508
+ mainEntryName: 'index',
2509
+ siteUrlGlobal: 'ULTRAMODERN_SITE_URL'
2510
+ },
2511
+ ...appHasEffectApi(app) ? {
2512
+ bff: {
2513
+ runtimeFramework: 'effect',
2514
+ prefix: app.effectApi.prefix,
2515
+ openapi: '/openapi.json'
2516
+ }
2517
+ } : {}
2518
+ };
2519
+ }
2520
+ function createStylingContract(enableTailwind) {
2521
+ return {
2522
+ tailwind: enableTailwind,
2523
+ ...enableTailwind ? {
2524
+ postcssPlugins: [
2525
+ '@tailwindcss/postcss'
2526
+ ],
2527
+ contentGlobs: [
2528
+ './src/**/*.{js,jsx,ts,tsx}'
2529
+ ]
2530
+ } : {}
2531
+ };
2532
+ }
2533
+ function createAppGeneratedContract(scope, app, apps, enableTailwind) {
2534
+ const remoteAppsForShell = apps.filter((candidate)=>'shell' !== candidate.kind && candidate.mfName);
2535
+ return {
2536
+ id: app.id,
2537
+ package: ultramodern_workspace_packageName(scope, app.packageSuffix),
2538
+ path: app.directory,
2539
+ kind: app.kind,
2540
+ config: createAppConfigContract(app),
2541
+ styling: createStylingContract(enableTailwind),
2542
+ deploy: {
2543
+ target: 'cloudflare',
2544
+ worker: {
2545
+ ssr: true
2546
+ },
2547
+ output: {
2548
+ flat: true,
2549
+ htmlDistPath: './'
2550
+ }
2551
+ },
2552
+ ssr: {
2553
+ mode: 'stream',
2554
+ moduleFederationAppSSR: true
2555
+ },
2556
+ i18n: {
2557
+ plugin: '@modern-js/plugin-i18n',
2558
+ backend: true,
2559
+ reactI18next: false,
2560
+ languages: [
2561
+ 'en',
2562
+ 'cs'
2563
+ ],
2564
+ fallbackLanguage: 'en',
2565
+ publicDir: './locales'
2566
+ },
2567
+ moduleFederation: {
2568
+ name: app.mfName,
2569
+ ...'shell' === app.kind ? {
2570
+ remotes: remoteAppsForShell.map((remote)=>({
2571
+ id: remote.id,
2572
+ alias: remoteDependencyAlias(remote),
2573
+ name: remote.mfName,
2574
+ manifestEnv: createRemoteManifestEnv(remote),
2575
+ manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
2576
+ }))
2577
+ } : {},
2578
+ exposes: Object.keys(app.exposes ?? {}),
2579
+ dts: {
2580
+ displayErrorInTerminal: true,
2581
+ compilerInstance: "--package typescript -- tsc"
2582
+ },
2583
+ browserSafeExposesOnly: true,
2584
+ zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION
2585
+ },
2586
+ marker: {
2587
+ appId: app.id,
2588
+ packageName: ultramodern_workspace_packageName(scope, app.packageSuffix),
2589
+ version: '0.1.0',
2590
+ build: createBuildMarker(scope, app),
2591
+ deployProfile: 'cloudflare-ssr-mf-effect-v1',
2592
+ uiSurface: 'ui',
2593
+ ...appHasEffectApi(app) ? {
2594
+ apiSurface: 'effect-bff'
2595
+ } : {}
2596
+ },
2597
+ ...appHasEffectApi(app) ? {
2598
+ effect: {
2599
+ runtime: 'effect',
2600
+ import: '@modern-js/plugin-bff/effect-edge',
2601
+ prefix: app.effectApi.prefix,
2602
+ openapi: '/openapi.json',
2603
+ workerEntry: 'worker/__modern_bff_effect.js',
2604
+ contract: './shared/effect/api',
2605
+ client: './effect/client',
2606
+ ...createEffectOperationContract(app)
2607
+ }
2608
+ } : {}
2609
+ };
2610
+ }
2611
+ function createGeneratedContract(scope, apps = [
2612
+ shellApp,
2613
+ ...remoteApps
2614
+ ], enableTailwind = true) {
2615
+ return {
2616
+ schemaVersion: 1,
2617
+ profile: 'cloudflare-ssr-mf-effect-v1',
2618
+ packageManager: {
2619
+ source: 'package.json',
2620
+ manager: 'pnpm',
2621
+ version: '11.3.0',
2622
+ toolchain: 'mise',
2623
+ corepack: false
2624
+ },
2625
+ versions: {
2626
+ typescript: TYPESCRIPT_VERSION,
2627
+ typescriptNativePreview: TYPESCRIPT_NATIVE_PREVIEW_VERSION,
2628
+ moduleFederation: MODULE_FEDERATION_VERSION,
2629
+ tanstackRouter: TANSTACK_ROUTER_VERSION,
2630
+ i18next: I18NEXT_VERSION,
2631
+ zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION,
2632
+ zephyrAgent: ZEPHYR_AGENT_VERSION,
2633
+ wrangler: WRANGLER_VERSION
2634
+ },
2635
+ apps: apps.map((app)=>createAppGeneratedContract(scope, app, apps, enableTailwind))
2636
+ };
2637
+ }
1839
2638
  function createTemplateManifest(modernVersion, packageSource) {
1840
2639
  return {
1841
2640
  schemaVersion: 1,
@@ -1913,6 +2712,12 @@ function createTemplateManifest(modernVersion, packageSource) {
1913
2712
  licensePath: '.agents/rstackjs-agent-skills-LICENSE'
1914
2713
  },
1915
2714
  baseline: baselineAgentSkills,
2715
+ moduleFederationSource: {
2716
+ repository: 'https://github.com/module-federation/agent-skills',
2717
+ commit: MODULE_FEDERATION_AGENT_SKILLS_COMMIT,
2718
+ install: 'clone',
2719
+ baseline: moduleFederationAgentSkills
2720
+ },
1916
2721
  privateSource: {
1917
2722
  repository: 'https://github.com/TechsioCZ/skills',
1918
2723
  install: 'clone-if-authorized',
@@ -1947,42 +2752,72 @@ function createTemplateManifest(modernVersion, packageSource) {
1947
2752
  }
1948
2753
  };
1949
2754
  }
1950
- function writeApp(targetDir, scope, app, packageSource) {
1951
- writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
1952
- writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
2755
+ function writeApp(targetDir, scope, app, packageSource, enableTailwind) {
2756
+ writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource, enableTailwind));
2757
+ writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory, appHasEffectApi(app)));
1953
2758
  writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n\ndeclare const ULTRAMODERN_SITE_URL: string;\n");
2759
+ writeFile(targetDir, `${app.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, app));
1954
2760
  writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
1955
- writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig());
1956
- writeJson(targetDir, `${app.directory}/config/public/locales/en/translation.json`, createEnglishTranslations(app));
1957
- writeJson(targetDir, `${app.directory}/config/public/locales/cs/translation.json`, createCzechTranslations(app));
2761
+ writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(app));
2762
+ writeJson(targetDir, `${app.directory}/locales/en/translation.json`, createAppLocaleMessages(app, 'en'));
2763
+ writeJson(targetDir, `${app.directory}/locales/cs/translation.json`, createAppLocaleMessages(app, 'cs'));
2764
+ writeFile(targetDir, `${app.directory}/src/routes/index.css`, createAppStyles(enableTailwind));
2765
+ if (enableTailwind) {
2766
+ writeFile(targetDir, `${app.directory}/postcss.config.mjs`, createPostcssConfig());
2767
+ writeFile(targetDir, `${app.directory}/tailwind.config.ts`, createTailwindConfig());
2768
+ }
1958
2769
  writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
1959
2770
  writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
1960
2771
  writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
1961
- if ('vertical' === app.kind) {
2772
+ if ('shell' === app.kind) writeFile(targetDir, `${app.directory}/src/effect/recommendations-client.ts`, createShellEffectClient(scope));
2773
+ if (appHasEffectApi(app)) {
2774
+ writeFile(targetDir, `${app.directory}/shared/effect/api.ts`, createEffectSharedApi(app));
2775
+ writeFile(targetDir, `${app.directory}/api/effect/index.ts`, createEffectServiceEntry(scope, app, '../../shared/effect/api'));
2776
+ writeFile(targetDir, `${app.directory}/src/effect/${app.effectApi.stem}-client.ts`, createEffectClient(app, '../../shared/effect/api'));
2777
+ }
2778
+ if ('vertical' === app.kind || 'horizontal-remote' === app.kind) {
1962
2779
  writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
1963
- const widgetFile = 'remote-identity' === app.id ? 'identity-widget.tsx' : 'commerce-widget.tsx';
1964
- writeFile(targetDir, `${app.directory}/src/components/${widgetFile}`, createRemoteWidget(app));
2780
+ writeFile(targetDir, `${app.directory}/src/components/${remoteWidgetFile(app)}.tsx`, createRemoteWidget(app));
1965
2781
  }
1966
2782
  if ('horizontal-design-system' === app.kind) {
1967
2783
  writeFile(targetDir, `${app.directory}/src/components/button.tsx`, createDesignButton());
1968
2784
  writeFile(targetDir, `${app.directory}/src/tokens.ts`, createDesignTokens());
1969
2785
  }
1970
2786
  }
1971
- function writeEffectService(targetDir, scope, packageSource) {
1972
- writeJson(targetDir, `${effectService.directory}/package.json`, createServicePackage(scope, packageSource));
1973
- writeJson(targetDir, `${effectService.directory}/tsconfig.json`, createPackageTsConfig(effectService.directory, true));
1974
- writeFile(targetDir, `${effectService.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
1975
- writeFile(targetDir, `${effectService.directory}/src/routes/page.tsx`, `export default function RecommendationsServiceHome() {
1976
- return <main>Recommendations Effect service</main>;
2787
+ function writeEffectService(targetDir, scope, packageSource, enableTailwind, service = effectService) {
2788
+ writeJson(targetDir, `${service.directory}/package.json`, createServicePackage(scope, packageSource, enableTailwind, service));
2789
+ writeJson(targetDir, `${service.directory}/tsconfig.json`, createPackageTsConfig(service.directory, true));
2790
+ writeFile(targetDir, `${service.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
2791
+ writeFile(targetDir, `${service.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, service));
2792
+ writeFile(targetDir, `${service.directory}/src/routes/layout.tsx`, createLayout(service.id));
2793
+ writeFile(targetDir, `${service.directory}/src/routes/page.tsx`, `import './index.css';
2794
+
2795
+ export default function ${toPascalCase(service.id)}Home() {
2796
+ return <main>${service.id} Effect service</main>;
1977
2797
  }
1978
2798
  `);
1979
- writeFile(targetDir, `${effectService.directory}/modern.config.ts`, createServiceModernConfig());
1980
- writeFile(targetDir, `${effectService.directory}/shared/effect/api.ts`, createEffectSharedApi());
1981
- writeFile(targetDir, `${effectService.directory}/api/effect/index.ts`, createEffectServiceEntry());
2799
+ writeFile(targetDir, `${service.directory}/src/routes/index.css`, createAppStyles(enableTailwind));
2800
+ if (enableTailwind) {
2801
+ writeFile(targetDir, `${service.directory}/postcss.config.mjs`, createPostcssConfig());
2802
+ writeFile(targetDir, `${service.directory}/tailwind.config.ts`, createTailwindConfig());
2803
+ }
2804
+ writeFile(targetDir, `${service.directory}/modern.config.ts`, createServiceModernConfigFor(service));
2805
+ writeFile(targetDir, `${service.directory}/api/effect/index.ts`, createEffectServiceEntry(scope, service));
2806
+ }
2807
+ function writeGenericSharedPackage(targetDir, scope, packageSource, sharedPackage) {
2808
+ writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description, packageSource));
2809
+ writeJson(targetDir, `${sharedPackage.directory}/tsconfig.json`, {
2810
+ extends: `${relativeRootFor(sharedPackage.directory)}/tsconfig.base.json`,
2811
+ include: [
2812
+ 'src'
2813
+ ]
2814
+ });
2815
+ writeFile(targetDir, `${sharedPackage.directory}/src/index.ts`, `export const packageId = '${sharedPackage.id}';
2816
+ `);
1982
2817
  }
1983
- function writeSharedPackages(targetDir, scope) {
2818
+ function writeSharedPackages(targetDir, scope, packageSource) {
1984
2819
  for (const sharedPackage of sharedPackages){
1985
- writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description));
2820
+ writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description, packageSource));
1986
2821
  writeJson(targetDir, `${sharedPackage.directory}/tsconfig.json`, {
1987
2822
  extends: `${relativeRootFor(sharedPackage.directory)}/tsconfig.base.json`,
1988
2823
  include: [
@@ -2004,26 +2839,286 @@ function writeSharedPackages(targetDir, scope) {
2004
2839
  },
2005
2840
  } as const;
2006
2841
  `);
2007
- writeFile(targetDir, 'packages/shared-effect-api/src/index.ts', `export interface Recommendation {
2008
- id: string;
2009
- title: string;
2842
+ writeFile(targetDir, 'packages/shared-effect-api/src/index.ts', createEffectSharedApi());
2010
2843
  }
2011
-
2012
- export const recommendationsApiContract = {
2013
- basePath: '/recommendations-api/effect/recommendations',
2014
- serviceId: '${effectService.id}',
2015
- } as const;
2016
- `);
2844
+ function readJsonFile(filePath) {
2845
+ return JSON.parse(node_fs.readFileSync(filePath, 'utf-8'));
2846
+ }
2847
+ function writeJsonFile(filePath, value) {
2848
+ node_fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
2849
+ }
2850
+ function appendEffectSharedApiContract(targetDir, service = effectService) {
2851
+ const relativePath = 'packages/shared-effect-api/src/index.ts';
2852
+ assertSafeRelativePath(relativePath);
2853
+ const filePath = node_path.join(targetDir, relativePath);
2854
+ ensureInsideRoot(targetDir, filePath);
2855
+ if (!node_fs.existsSync(filePath)) throw new Error(`Missing generated Effect API package: ${relativePath}`);
2856
+ const current = node_fs.readFileSync(filePath, 'utf-8');
2857
+ const apiExport = serviceEffectApiExport(service);
2858
+ if (current.includes(`export const ${apiExport} =`)) return;
2859
+ const contentWithImports = current.includes('@modern-js/plugin-bff/effect-client') ? current.trimEnd() : `${createEffectSharedApiImports()}\n${current.trimEnd()}`;
2860
+ node_fs.writeFileSync(filePath, `${contentWithImports}\n\n${createEffectSharedApiContract(service)}`, 'utf-8');
2861
+ }
2862
+ function existingPackageSource(workspaceRoot, modernVersion, packageSource) {
2863
+ if (packageSource) return resolvePackageSource({
2864
+ targetDir: workspaceRoot,
2865
+ packageName: node_path.basename(workspaceRoot),
2866
+ modernVersion,
2867
+ packageSource
2868
+ });
2869
+ const metadataPath = node_path.join(workspaceRoot, '.modernjs/ultramodern-package-source.json');
2870
+ if (!node_fs.existsSync(metadataPath)) return resolvePackageSource({
2871
+ targetDir: workspaceRoot,
2872
+ packageName: node_path.basename(workspaceRoot),
2873
+ modernVersion
2874
+ });
2875
+ const metadata = readJsonFile(metadataPath);
2876
+ const aliases = metadata.modernPackages?.aliases ?? {};
2877
+ const firstAlias = Object.values(aliases).find((value)=>'string' == typeof value);
2878
+ const firstPackage = Object.keys(aliases)[0];
2879
+ const aliasScope = firstAlias?.match(/^@([^/]+)\//)?.[1];
2880
+ const unscopedName = firstPackage?.split('/').at(-1) ?? '';
2881
+ const aliasUnscopedName = firstAlias?.split('/').at(-1) ?? '';
2882
+ const aliasPackageNamePrefix = aliasUnscopedName && unscopedName && aliasUnscopedName.endsWith(unscopedName) ? aliasUnscopedName.slice(0, -unscopedName.length) : void 0;
2883
+ return {
2884
+ strategy: 'install' === metadata.strategy ? 'install' : 'workspace',
2885
+ modernPackageVersion: 'string' == typeof metadata.modernPackages?.specifier ? metadata.modernPackages.specifier : modernVersion,
2886
+ registry: metadata.modernPackages?.registry,
2887
+ aliasScope,
2888
+ aliasPackageNamePrefix
2889
+ };
2890
+ }
2891
+ function assertValidMicroVerticalName(name) {
2892
+ const normalized = toKebabCase(name);
2893
+ if (!normalized || normalized !== name) throw new Error(`Invalid MicroVertical name "${name}". Use lowercase kebab-case.`);
2894
+ return normalized;
2895
+ }
2896
+ function nextAvailablePort(ports) {
2897
+ const numericPorts = Object.values(ports).filter((value)=>'number' == typeof value && Number.isFinite(value));
2898
+ return Math.max(3030, ...numericPorts) + 1;
2899
+ }
2900
+ function assertCanCreate(workspaceRoot, relativePath) {
2901
+ if (node_fs.existsSync(node_path.join(workspaceRoot, relativePath))) throw new Error(`Refusing to overwrite existing path: ${relativePath}`);
2902
+ }
2903
+ function addRootDevScript(workspaceRoot, scope, packageSuffix, scriptName) {
2904
+ const packagePath = node_path.join(workspaceRoot, 'package.json');
2905
+ const rootPackage = readJsonFile(packagePath);
2906
+ rootPackage.scripts ??= {};
2907
+ rootPackage.scripts[`dev:${scriptName}`] = `pnpm --filter ${ultramodern_workspace_packageName(scope, packageSuffix)} dev`;
2908
+ if ('string' == typeof rootPackage.scripts.dev && !rootPackage.scripts.dev.includes(ultramodern_workspace_packageName(scope, packageSuffix))) {
2909
+ const packageFilter = `--filter ${ultramodern_workspace_packageName(scope, packageSuffix)}`;
2910
+ rootPackage.scripts.dev = rootPackage.scripts.dev.endsWith(' dev') ? rootPackage.scripts.dev.replace(/ dev$/u, ` ${packageFilter} dev`) : `${rootPackage.scripts.dev} ${packageFilter}`;
2911
+ }
2912
+ writeJsonFile(packagePath, rootPackage);
2913
+ }
2914
+ function addShellZephyrDependency(workspaceRoot, scope, remote) {
2915
+ const packagePath = node_path.join(workspaceRoot, shellApp.directory, 'package.json');
2916
+ const shellPackage = readJsonFile(packagePath);
2917
+ shellPackage['zephyr:dependencies'] ??= {};
2918
+ shellPackage['zephyr:dependencies'][remoteDependencyAlias(remote)] = zephyrRemoteDependency(scope, remote);
2919
+ writeJsonFile(packagePath, shellPackage);
2920
+ }
2921
+ function addShellWorkspaceDependency(workspaceRoot, scope, remote) {
2922
+ if (!appHasEffectApi(remote)) return;
2923
+ const packagePath = node_path.join(workspaceRoot, shellApp.directory, 'package.json');
2924
+ const shellPackage = readJsonFile(packagePath);
2925
+ shellPackage.dependencies ??= {};
2926
+ shellPackage.dependencies[ultramodern_workspace_packageName(scope, remote.packageSuffix)] = WORKSPACE_PACKAGE_VERSION;
2927
+ writeJsonFile(packagePath, shellPackage);
2928
+ }
2929
+ function remoteTopologyEntry(scope, remote) {
2930
+ return {
2931
+ id: remote.id,
2932
+ kind: remote.kind,
2933
+ domain: remote.domain,
2934
+ package: ultramodern_workspace_packageName(scope, remote.packageSuffix),
2935
+ moduleFederation: {
2936
+ role: 'remote',
2937
+ name: remote.mfName,
2938
+ manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`,
2939
+ exposes: Object.keys(remote.exposes ?? {}),
2940
+ ssr: true,
2941
+ fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
2942
+ sharedContractVersion: 'mf-ssr-contract-v1'
2943
+ },
2944
+ ...effectApiTopologyMetadata(remote) ? {
2945
+ api: effectApiTopologyMetadata(remote)
2946
+ } : {},
2947
+ ownership: remote.ownership
2948
+ };
2949
+ }
2950
+ function ownershipEntry(scope, owner) {
2951
+ return {
2952
+ id: owner.id,
2953
+ package: ultramodern_workspace_packageName(scope, owner.packageSuffix),
2954
+ path: owner.directory,
2955
+ ownership: owner.ownership
2956
+ };
2957
+ }
2958
+ function remotesFromTopology(topology, ports) {
2959
+ return (topology.remotes ?? []).map((remote)=>{
2960
+ const effectApi = remote.api?.effect ? {
2961
+ stem: 'string' == typeof remote.api.effect.basePath ? remote.api.effect.basePath.split('/').filter(Boolean).at(-1) ?? remote.domain ?? String(remote.id).replace(/^remote-/, '') : remote.domain ?? String(remote.id).replace(/^remote-/, ''),
2962
+ prefix: remote.api.effect.bff?.prefix ?? `/${remote.domain ?? String(remote.id).replace(/^remote-/, '')}-api`,
2963
+ consumedBy: Array.isArray(remote.api.effect.consumedBy) ? remote.api.effect.consumedBy : [
2964
+ shellApp.id,
2965
+ remote.id
2966
+ ]
2967
+ } : void 0;
2968
+ return {
2969
+ id: remote.id,
2970
+ directory: '',
2971
+ packageSuffix: remote.package?.split('/').at(-1) ?? remote.id,
2972
+ displayName: remote.id,
2973
+ kind: remote.kind ?? 'vertical',
2974
+ domain: remote.domain ?? String(remote.id).replace(/^remote-/, ''),
2975
+ portEnv: '',
2976
+ port: 'number' == typeof ports[remote.id] ? ports[remote.id] : 0,
2977
+ mfName: remote.moduleFederation?.name ?? `remote${toPascalCase(remote.id)}`,
2978
+ ...effectApi ? {
2979
+ effectApi
2980
+ } : {},
2981
+ ownership: remote.ownership ?? createNeutralOwnership(remote.id)
2982
+ };
2983
+ });
2984
+ }
2985
+ function addUltramodernMicroVertical(options) {
2986
+ const name = assertValidMicroVerticalName(options.name);
2987
+ const rootPackage = readJsonFile(node_path.join(options.workspaceRoot, 'package.json'));
2988
+ const scope = toPackageScope(String(rootPackage.name ?? node_path.basename(options.workspaceRoot)));
2989
+ const topologyPath = node_path.join(options.workspaceRoot, 'topology/reference-topology.json');
2990
+ const ownershipPath = node_path.join(options.workspaceRoot, 'topology/ownership.json');
2991
+ const overlayPath = node_path.join(options.workspaceRoot, 'topology/local-overlays/development.json');
2992
+ for (const requiredPath of [
2993
+ topologyPath,
2994
+ ownershipPath,
2995
+ overlayPath
2996
+ ])if (!node_fs.existsSync(requiredPath)) throw new Error(`Missing UltraModern workspace file: ${requiredPath}`);
2997
+ const topology = readJsonFile(topologyPath);
2998
+ const ownership = readJsonFile(ownershipPath);
2999
+ const overlay = readJsonFile(overlayPath);
3000
+ overlay.ports ??= {};
3001
+ const packageSource = existingPackageSource(options.workspaceRoot, options.modernVersion, options.packageSource);
3002
+ const enableTailwind = false !== options.enableTailwind;
3003
+ const port = nextAvailablePort(overlay.ports);
3004
+ if ('remote' === options.kind || 'horizontal-remote' === options.kind) {
3005
+ const remote = createRemoteDescriptor(name, options.kind, port);
3006
+ assertCanCreate(options.workspaceRoot, remote.directory);
3007
+ if ((topology.remotes ?? []).some((entry)=>entry.id === remote.id)) throw new Error(`Topology already contains ${remote.id}`);
3008
+ if (Object.values(overlay.ports).includes(remote.port)) throw new Error(`Development port ${remote.port} is already in use`);
3009
+ writeApp(options.workspaceRoot, scope, remote, packageSource, enableTailwind);
3010
+ topology.shell ??= {};
3011
+ topology.shell.remoteRefs ??= [];
3012
+ topology.shell.remoteRefs.push(remote.id);
3013
+ topology.shell.moduleFederation ??= {};
3014
+ topology.shell.moduleFederation.remotes ??= [];
3015
+ topology.shell.moduleFederation.remotes.push({
3016
+ id: remote.id,
3017
+ name: remote.mfName,
3018
+ manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
3019
+ });
3020
+ topology.remotes ??= [];
3021
+ topology.remotes.push(remoteTopologyEntry(scope, remote));
3022
+ ownership.owners ??= [];
3023
+ ownership.owners.push(ownershipEntry(scope, remote));
3024
+ overlay.ports[remote.id] = remote.port;
3025
+ overlay.manifests ??= {};
3026
+ overlay.manifests[remote.id] = `http://localhost:${remote.port}/mf-manifest.json`;
3027
+ if (appHasEffectApi(remote)) {
3028
+ overlay.apis ??= {};
3029
+ overlay.apis[remote.id] = `http://localhost:${remote.port}${effectApiPrefix(remote)}`;
3030
+ }
3031
+ writeJsonFile(topologyPath, topology);
3032
+ writeJsonFile(ownershipPath, ownership);
3033
+ writeJsonFile(overlayPath, overlay);
3034
+ writeJsonFile(node_path.join(options.workspaceRoot, GENERATED_CONTRACT_PATH), createGeneratedContract(scope, [
3035
+ shellApp,
3036
+ ...remotesFromTopology(topology, overlay.ports)
3037
+ ], enableTailwind));
3038
+ const shellConfigPath = node_path.join(options.workspaceRoot, `${shellApp.directory}/module-federation.config.ts`);
3039
+ writeFileReplacing(options.workspaceRoot, `${shellApp.directory}/module-federation.config.ts`, createShellModuleFederationConfig(remotesFromTopology(topology, overlay.ports)));
3040
+ if (!node_fs.existsSync(shellConfigPath)) throw new Error('Shell Module Federation config was not regenerated');
3041
+ addShellZephyrDependency(options.workspaceRoot, scope, remote);
3042
+ addShellWorkspaceDependency(options.workspaceRoot, scope, remote);
3043
+ addRootDevScript(options.workspaceRoot, scope, remote.packageSuffix, name);
3044
+ return;
3045
+ }
3046
+ if ('service' === options.kind) {
3047
+ const service = createServiceDescriptor(name, port);
3048
+ assertCanCreate(options.workspaceRoot, service.directory);
3049
+ if ((topology.effectServices ?? []).some((entry)=>entry.id === service.id)) throw new Error(`Topology already contains ${service.id}`);
3050
+ writeEffectService(options.workspaceRoot, scope, packageSource, enableTailwind, service);
3051
+ appendEffectSharedApiContract(options.workspaceRoot, service);
3052
+ topology.effectServices ??= [];
3053
+ topology.effectServices.push({
3054
+ id: service.id,
3055
+ kind: 'effect-service',
3056
+ runtime: 'effect',
3057
+ package: ultramodern_workspace_packageName(scope, service.packageSuffix),
3058
+ consumedBy: [
3059
+ shellApp.id
3060
+ ],
3061
+ bff: {
3062
+ prefix: serviceApiPrefix(service),
3063
+ openapi: '/openapi.json'
3064
+ },
3065
+ contract: {
3066
+ package: ultramodern_workspace_packageName(scope, 'shared-effect-api'),
3067
+ export: serviceEffectApiExport(service),
3068
+ path: 'packages/shared-effect-api/src/index.ts'
3069
+ },
3070
+ serverEntry: `${service.directory}/api/effect/index.ts`,
3071
+ basePath: `${serviceApiPrefix(service)}/effect/${effectApiStem(service)}`,
3072
+ ...createEffectOperationContract(service),
3073
+ ownership: service.ownership
3074
+ });
3075
+ ownership.owners ??= [];
3076
+ ownership.owners.push(ownershipEntry(scope, service));
3077
+ overlay.ports[service.id] = service.port;
3078
+ overlay.services ??= {};
3079
+ overlay.services[service.id] = `http://localhost:${service.port}${serviceApiPrefix(service)}`;
3080
+ writeJsonFile(topologyPath, topology);
3081
+ writeJsonFile(ownershipPath, ownership);
3082
+ writeJsonFile(overlayPath, overlay);
3083
+ addRootDevScript(options.workspaceRoot, scope, service.packageSuffix, name);
3084
+ return;
3085
+ }
3086
+ if ('shared' === options.kind) {
3087
+ const sharedPackage = createSharedPackageDescriptor(name);
3088
+ assertCanCreate(options.workspaceRoot, sharedPackage.directory);
3089
+ if ((topology.sharedPackages ?? []).some((entry)=>entry.id === sharedPackage.id)) throw new Error(`Topology already contains ${sharedPackage.id}`);
3090
+ writeGenericSharedPackage(options.workspaceRoot, scope, packageSource, sharedPackage);
3091
+ topology.sharedPackages ??= [];
3092
+ topology.sharedPackages.push({
3093
+ id: sharedPackage.id,
3094
+ package: ultramodern_workspace_packageName(scope, sharedPackage.id),
3095
+ path: sharedPackage.directory,
3096
+ description: sharedPackage.description
3097
+ });
3098
+ ownership.owners ??= [];
3099
+ ownership.owners.push(ownershipEntry(scope, {
3100
+ id: sharedPackage.id,
3101
+ packageSuffix: sharedPackage.id,
3102
+ directory: sharedPackage.directory,
3103
+ ownership: createNeutralOwnership(sharedPackage.id, 'tier-1-shared-contract')
3104
+ }));
3105
+ writeJsonFile(topologyPath, topology);
3106
+ writeJsonFile(ownershipPath, ownership);
3107
+ return;
3108
+ }
3109
+ throw new Error(`Unsupported MicroVertical kind: ${options.kind}`);
2017
3110
  }
2018
3111
  function generateUltramodernWorkspace(options) {
2019
3112
  const scope = toPackageScope(options.packageName);
2020
3113
  const packageSource = resolvePackageSource(options);
3114
+ const enableTailwind = false !== options.enableTailwind;
2021
3115
  node_fs.mkdirSync(options.targetDir, {
2022
3116
  recursive: true
2023
3117
  });
2024
3118
  copyRootTemplate(options.targetDir, {
2025
3119
  packageName: options.packageName,
2026
- packageScope: scope
3120
+ packageScope: scope,
3121
+ tailwindEnabled: String(enableTailwind)
2027
3122
  });
2028
3123
  writeJson(options.targetDir, 'package.json', createRootPackageJson(scope, packageSource));
2029
3124
  writeJson(options.targetDir, 'tsconfig.base.json', createTsConfigBase());
@@ -2032,10 +3127,13 @@ function generateUltramodernWorkspace(options) {
2032
3127
  writeJson(options.targetDir, 'topology/local-overlays/development.json', createDevelopmentOverlay());
2033
3128
  writeJson(options.targetDir, '.modernjs/ultramodern-workspace-template-manifest.json', createTemplateManifest(options.modernVersion, packageSource));
2034
3129
  writeJson(options.targetDir, '.modernjs/ultramodern-package-source.json', createPackageSourceMetadata(scope, packageSource));
2035
- writeApp(options.targetDir, scope, shellApp, packageSource);
2036
- for (const remote of remoteApps)writeApp(options.targetDir, scope, remote, packageSource);
2037
- writeEffectService(options.targetDir, scope, packageSource);
2038
- writeSharedPackages(options.targetDir, scope);
3130
+ writeJson(options.targetDir, GENERATED_CONTRACT_PATH, createGeneratedContract(scope, [
3131
+ shellApp,
3132
+ ...remoteApps
3133
+ ], enableTailwind));
3134
+ writeApp(options.targetDir, scope, shellApp, packageSource, enableTailwind);
3135
+ for (const remote of remoteApps)writeApp(options.targetDir, scope, remote, packageSource, enableTailwind);
3136
+ writeSharedPackages(options.targetDir, scope, packageSource);
2039
3137
  }
2040
3138
  const src_dirname = node_path.dirname(fileURLToPath(import.meta.url));
2041
3139
  const templateDir = node_path.resolve(src_dirname, '..', 'template');
@@ -2045,6 +3143,9 @@ const sha1Pattern = /^[0-9a-f]{40}$/;
2045
3143
  const sha256Pattern = /^[0-9a-f]{64}$/;
2046
3144
  const templateIdPattern = /^[a-z0-9][a-z0-9._-]*$/;
2047
3145
  const packageNamePattern = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
3146
+ const src_TANSTACK_ROUTER_VERSION = '1.170.8';
3147
+ const src_TAILWIND_VERSION = '4.3.0';
3148
+ const src_TAILWIND_POSTCSS_VERSION = '4.3.0';
2048
3149
  const requiredDeniedPaths = [
2049
3150
  '.git/**',
2050
3151
  '.npmrc',
@@ -2089,8 +3190,8 @@ function detectRouterFramework() {
2089
3190
  '--router',
2090
3191
  '-r'
2091
3192
  ]);
2092
- if (!routerValue || 'react-router' === routerValue) return 'react-router';
2093
- if ('tanstack' === routerValue) return 'tanstack';
3193
+ if (!routerValue || 'tanstack' === routerValue) return 'tanstack';
3194
+ if ('react-router' === routerValue) return 'react-router';
2094
3195
  console.error(i18n.t(localeKeys.error.invalidRouter, {
2095
3196
  router: routerValue
2096
3197
  }));
@@ -2231,6 +3332,7 @@ function createBuiltinTemplateManifest(version) {
2231
3332
  '.browserslistrc',
2232
3333
  '.github/**',
2233
3334
  '.gitignore',
3335
+ '.mise.toml',
2234
3336
  '.modernjs/**',
2235
3337
  '.nvmrc',
2236
3338
  'AGENTS.md',
@@ -2430,6 +3532,7 @@ function showHelp() {
2430
3532
  if (localeKeys.help.optionUltramodernPackageSource) console.log(i18n.t(localeKeys.help.optionUltramodernPackageSource));
2431
3533
  if (localeKeys.help.optionUltramodernPackageScope) console.log(i18n.t(localeKeys.help.optionUltramodernPackageScope));
2432
3534
  if (localeKeys.help.optionUltramodernPackageNamePrefix) console.log(i18n.t(localeKeys.help.optionUltramodernPackageNamePrefix));
3535
+ if (localeKeys.help.optionMicroVertical) console.log(i18n.t(localeKeys.help.optionMicroVertical));
2433
3536
  console.log(i18n.t(localeKeys.help.optionSub));
2434
3537
  console.log('');
2435
3538
  console.log(i18n.t(localeKeys.help.examples));
@@ -2443,6 +3546,7 @@ function showHelp() {
2443
3546
  if (localeKeys.help.example8) console.log(i18n.t(localeKeys.help.example8));
2444
3547
  if (localeKeys.help.example9) console.log(i18n.t(localeKeys.help.example9));
2445
3548
  if (localeKeys.help.example10) console.log(i18n.t(localeKeys.help.example10));
3549
+ if (localeKeys.help.example11) console.log(i18n.t(localeKeys.help.example11));
2446
3550
  console.log('');
2447
3551
  console.log(i18n.t(localeKeys.help.moreInfo));
2448
3552
  console.log('');
@@ -2468,12 +3572,21 @@ function detectSubprojectFlag() {
2468
3572
  }
2469
3573
  function detectTailwindFlag() {
2470
3574
  const args = process.argv.slice(2);
2471
- return args.includes('--tailwind');
3575
+ return !args.includes('--no-tailwind');
2472
3576
  }
2473
3577
  function detectWorkspaceProtocolFlag() {
2474
3578
  const args = process.argv.slice(2);
2475
3579
  return args.includes('--workspace');
2476
3580
  }
3581
+ function detectMicroVerticalKind() {
3582
+ const kind = getOptionValue(process.argv.slice(2), [
3583
+ '--microvertical'
3584
+ ]);
3585
+ if (!kind) return;
3586
+ if ('remote' === kind || 'horizontal-remote' === kind || 'service' === kind || 'shared' === kind) return kind;
3587
+ console.error('--microvertical must be one of: remote, horizontal-remote, service, shared');
3588
+ process.exit(1);
3589
+ }
2477
3590
  function detectUltramodernWorkspaceFlag(createPackage) {
2478
3591
  const args = process.argv.slice(2);
2479
3592
  return args.includes(ULTRAMODERN_WORKSPACE_FLAG) || isBleedingDevCreatePackage(createPackage);
@@ -2574,7 +3687,8 @@ async function getProjectName() {
2574
3687
  '--ultramodern-package-version',
2575
3688
  '--ultramodern-package-registry',
2576
3689
  '--ultramodern-package-scope',
2577
- '--ultramodern-package-name-prefix'
3690
+ '--ultramodern-package-name-prefix',
3691
+ '--microvertical'
2578
3692
  ]);
2579
3693
  const optionWithoutValue = new Set([
2580
3694
  '--help',
@@ -2587,6 +3701,7 @@ async function getProjectName() {
2587
3701
  '--tanstack',
2588
3702
  '--bff',
2589
3703
  '--tailwind',
3704
+ '--no-tailwind',
2590
3705
  '--workspace',
2591
3706
  ULTRAMODERN_WORKSPACE_FLAG
2592
3707
  ]);
@@ -2598,7 +3713,7 @@ async function getProjectName() {
2598
3713
  i += 1;
2599
3714
  continue;
2600
3715
  }
2601
- if (!(arg.startsWith('--lang=') || arg.startsWith('--router=') || arg.startsWith('--bff-runtime=') || arg.startsWith('--ultramodern-package-source=') || arg.startsWith('--ultramodern-package-version=') || arg.startsWith('--ultramodern-package-registry=') || arg.startsWith('--ultramodern-package-scope=') || arg.startsWith('--ultramodern-package-name-prefix='))) positionalArgs.push(arg);
3716
+ if (!(arg.startsWith('--lang=') || arg.startsWith('--router=') || arg.startsWith('--bff-runtime=') || arg.startsWith('--ultramodern-package-source=') || arg.startsWith('--ultramodern-package-version=') || arg.startsWith('--ultramodern-package-registry=') || arg.startsWith('--ultramodern-package-scope=') || arg.startsWith('--ultramodern-package-name-prefix=') || arg.startsWith('--microvertical='))) positionalArgs.push(arg);
2602
3717
  }
2603
3718
  }
2604
3719
  const projectNameArg = positionalArgs[0];
@@ -2629,6 +3744,26 @@ async function main() {
2629
3744
  const { name: projectName, useCurrentDir } = await getProjectName();
2630
3745
  const targetDir = useCurrentDir ? process.cwd() : node_path.isAbsolute(projectName) ? projectName : node_path.resolve(process.cwd(), projectName);
2631
3746
  const generatedPackageName = useCurrentDir || node_path.isAbsolute(projectName) ? node_path.basename(targetDir) : projectName;
3747
+ const createPackage = readCreatePackageJson();
3748
+ const version = createPackage.version || 'latest';
3749
+ const ultramodernPackageVersion = isBleedingDevCreatePackage(createPackage) ? getBleedingDevFrameworkVersion(createPackage, version) : version;
3750
+ const microVerticalKind = detectMicroVerticalKind();
3751
+ if (microVerticalKind) {
3752
+ const overridePackageSource = args.some((arg)=>arg.startsWith('--ultramodern-package-')) ? detectUltramodernPackageSource(args, ultramodernPackageVersion, createPackage) : void 0;
3753
+ addUltramodernMicroVertical({
3754
+ workspaceRoot: process.cwd(),
3755
+ name: generatedPackageName,
3756
+ kind: microVerticalKind,
3757
+ modernVersion: version,
3758
+ enableTailwind: detectTailwindFlag(),
3759
+ packageSource: overridePackageSource
3760
+ });
3761
+ const dim = '\x1b[2m\x1b[3m';
3762
+ const reset = '\x1b[0m';
3763
+ console.log(`${i18n.t(localeKeys.message.success)}\n`);
3764
+ console.log(`${dim} pnpm ultramodern:check${reset}\n`);
3765
+ return;
3766
+ }
2632
3767
  if (node_fs.existsSync(targetDir)) {
2633
3768
  const files = node_fs.readdirSync(targetDir);
2634
3769
  if (files.length > 0) {
@@ -2638,15 +3773,13 @@ async function main() {
2638
3773
  process.exit(1);
2639
3774
  }
2640
3775
  }
2641
- const createPackage = readCreatePackageJson();
2642
- const version = createPackage.version || 'latest';
2643
- const ultramodernPackageVersion = isBleedingDevCreatePackage(createPackage) ? getBleedingDevFrameworkVersion(createPackage, version) : version;
2644
3776
  const generateWorkspace = detectUltramodernWorkspaceFlag(createPackage);
2645
3777
  if (generateWorkspace) {
2646
3778
  generateUltramodernWorkspace({
2647
3779
  targetDir,
2648
3780
  packageName: generatedPackageName,
2649
3781
  modernVersion: version,
3782
+ enableTailwind: detectTailwindFlag(),
2650
3783
  packageSource: detectUltramodernPackageSource(args, ultramodernPackageVersion, createPackage)
2651
3784
  });
2652
3785
  const dim = '\x1b[2m\x1b[3m';
@@ -2680,6 +3813,9 @@ async function main() {
2680
3813
  pluginTanstackVersion: singleAppModernPackageSpecifier('@modern-js/plugin-tanstack', packageSource, useWorkspaceProtocol),
2681
3814
  pluginBffVersion: singleAppModernPackageSpecifier('@modern-js/plugin-bff', packageSource, useWorkspaceProtocol),
2682
3815
  pluginI18nVersion: singleAppModernPackageSpecifier('@modern-js/plugin-i18n', packageSource, useWorkspaceProtocol),
3816
+ tanstackRouterVersion: src_TANSTACK_ROUTER_VERSION,
3817
+ tailwindVersion: src_TAILWIND_VERSION,
3818
+ tailwindPostcssVersion: src_TAILWIND_POSTCSS_VERSION,
2683
3819
  isSubproject,
2684
3820
  routerFramework,
2685
3821
  bffRuntime,
@@ -2771,6 +3907,9 @@ function copyTemplate(src, dest, options) {
2771
3907
  pluginTanstackVersion: options.pluginTanstackVersion,
2772
3908
  pluginBffVersion: options.pluginBffVersion,
2773
3909
  pluginI18nVersion: options.pluginI18nVersion,
3910
+ tanstackRouterVersion: options.tanstackRouterVersion,
3911
+ tailwindVersion: options.tailwindVersion,
3912
+ tailwindPostcssVersion: options.tailwindPostcssVersion,
2774
3913
  isSubproject: options.isSubproject,
2775
3914
  isTanstackRouter: 'tanstack' === options.routerFramework,
2776
3915
  enableBff: 'none' !== options.bffRuntime,