@bleedingdev/modern-js-create 3.2.0-ultramodern.3 → 3.2.0-ultramodern.30
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/README.md +32 -14
- package/dist/index.js +2753 -324
- package/dist/types/locale/en.d.ts +2 -0
- package/dist/types/locale/zh.d.ts +2 -0
- package/dist/types/ultramodern-workspace.d.ts +13 -0
- package/package.json +8 -5
- package/template/.agents/skills-lock.json +34 -0
- package/template/.github/renovate.json +53 -0
- package/template/.github/workflows/ultramodern-gates.yml.handlebars +34 -10
- package/template/.mise.toml.handlebars +2 -0
- package/template/AGENTS.md +27 -0
- package/template/README.md +20 -14
- package/template/api/effect/index.ts.handlebars +7 -45
- package/template/config/public/locales/cs/translation.json +39 -0
- package/template/config/public/locales/en/translation.json +39 -0
- package/template/modern.config.ts.handlebars +44 -23
- package/template/oxfmt.config.ts +8 -0
- package/template/oxlint.config.ts +12 -0
- package/template/package.json.handlebars +59 -30
- package/template/pnpm-workspace.yaml +26 -0
- package/template/rstest.config.mts +7 -0
- package/template/scripts/bootstrap-agent-skills.mjs +95 -0
- package/template/scripts/check-i18n-strings.mjs +83 -0
- package/template/scripts/validate-ultramodern.mjs.handlebars +438 -16
- package/template/shared/effect/api.ts.handlebars +1 -2
- package/template/src/modern-app-env.d.ts +2 -0
- package/template/src/modern.runtime.ts.handlebars +17 -3
- package/template/src/routes/[lang]/page.tsx.handlebars +210 -0
- package/template/src/routes/index.css.handlebars +14 -3
- package/template/src/routes/layout.tsx.handlebars +2 -1
- package/template/tests/tsconfig.json +7 -0
- package/template/tests/ultramodern.contract.test.ts.handlebars +78 -0
- package/template/tsconfig.json +106 -2
- package/template-workspace/.agents/agent-reference-repos.json +24 -0
- package/template-workspace/.agents/rstackjs-agent-skills-LICENSE +21 -0
- package/template-workspace/.agents/skills/rsbuild-best-practices/SKILL.md +57 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/SKILL.md +96 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/command-map.md +113 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/common-analysis-patterns.md +190 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-common.md +88 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md +138 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md +71 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/install-rsdoctor.md +39 -0
- package/template-workspace/.agents/skills/rsdoctor-analysis/references/rsdoctor-data-types.md +103 -0
- package/template-workspace/.agents/skills/rslib-best-practices/SKILL.md +58 -0
- package/template-workspace/.agents/skills/rslib-modern-package/SKILL.md +173 -0
- package/template-workspace/.agents/skills/rspack-best-practices/SKILL.md +70 -0
- package/template-workspace/.agents/skills/rspack-tracing/SKILL.md +75 -0
- package/template-workspace/.agents/skills/rspack-tracing/references/bottlenecks.md +47 -0
- package/template-workspace/.agents/skills/rspack-tracing/references/tracing-guide.md +38 -0
- package/template-workspace/.agents/skills/rspack-tracing/scripts/analyze_trace.js +184 -0
- package/template-workspace/.agents/skills/rstest-best-practices/SKILL.md +133 -0
- package/template-workspace/.agents/skills-lock.json +114 -0
- package/template-workspace/.github/renovate.json +29 -0
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +54 -0
- package/template-workspace/.gitignore.handlebars +5 -0
- package/template-workspace/.mise.toml.handlebars +2 -0
- package/template-workspace/AGENTS.md +61 -0
- package/template-workspace/README.md.handlebars +26 -5
- package/template-workspace/oxfmt.config.ts +16 -0
- package/template-workspace/oxlint.config.ts +19 -0
- package/template-workspace/pnpm-workspace.yaml +34 -8
- package/template-workspace/scripts/assert-mf-types.mjs +44 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +155 -0
- package/template-workspace/scripts/setup-agent-reference-repos.mjs +364 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +755 -87
- package/template/biome.json +0 -41
- package/template/src/routes/page.tsx.handlebars +0 -119
package/dist/index.js
CHANGED
|
@@ -453,8 +453,8 @@ const EN_LOCALE = {
|
|
|
453
453
|
success: '✨ Created successfully!',
|
|
454
454
|
nextSteps: '📋 Next steps:',
|
|
455
455
|
step1: 'cd {projectName}',
|
|
456
|
-
step2: 'pnpm install',
|
|
457
|
-
step3: 'pnpm dev'
|
|
456
|
+
step2: 'mise exec -- pnpm install',
|
|
457
|
+
step3: 'mise exec -- pnpm dev'
|
|
458
458
|
},
|
|
459
459
|
help: {
|
|
460
460
|
title: '🚀 Modern.js Project Creator',
|
|
@@ -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
|
|
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
|
-
example10: ' create my-super-app
|
|
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: {
|
|
@@ -508,8 +510,8 @@ const ZH_LOCALE = {
|
|
|
508
510
|
success: '✨ 创建成功!',
|
|
509
511
|
nextSteps: '📋 下一步:',
|
|
510
512
|
step1: 'cd {projectName}',
|
|
511
|
-
step2: 'pnpm install',
|
|
512
|
-
step3: 'pnpm dev'
|
|
513
|
+
step2: 'mise exec -- pnpm install',
|
|
514
|
+
step3: 'mise exec -- pnpm dev'
|
|
513
515
|
},
|
|
514
516
|
help: {
|
|
515
517
|
title: '🚀 Modern.js 项目创建工具',
|
|
@@ -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
|
|
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,15 +558,51 @@ 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.
|
|
558
|
-
const MODULE_FEDERATION_VERSION = '2.
|
|
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';
|
|
559
569
|
const TYPESCRIPT_VERSION = '6.0.3';
|
|
570
|
+
const TYPESCRIPT_NATIVE_PREVIEW_VERSION = '7.0.0-dev.20260527.2';
|
|
571
|
+
const OXLINT_VERSION = '1.66.0';
|
|
572
|
+
const OXFMT_VERSION = '0.51.0';
|
|
573
|
+
const ULTRACITE_VERSION = '7.7.0';
|
|
574
|
+
const I18NEXT_VERSION = '26.2.0';
|
|
560
575
|
const REACT_VERSION = '^19.2.6';
|
|
561
576
|
const REACT_DOM_VERSION = '^19.2.6';
|
|
577
|
+
const PNPM_VERSION = '11.4.0';
|
|
562
578
|
const WORKSPACE_PACKAGE_VERSION = 'workspace:*';
|
|
579
|
+
const GENERATED_CONTRACT_PATH = '.modernjs/ultramodern-generated-contract.json';
|
|
580
|
+
const RSTACK_AGENT_SKILLS_COMMIT = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
581
|
+
const MODULE_FEDERATION_AGENT_SKILLS_COMMIT = '07bb5b6c43ad457609e00c081b72d4c42508ec76';
|
|
582
|
+
const baselineAgentSkills = [
|
|
583
|
+
'rsbuild-best-practices',
|
|
584
|
+
'rspack-best-practices',
|
|
585
|
+
'rspack-tracing',
|
|
586
|
+
'rsdoctor-analysis',
|
|
587
|
+
'rslib-best-practices',
|
|
588
|
+
'rslib-modern-package',
|
|
589
|
+
'rstest-best-practices'
|
|
590
|
+
];
|
|
591
|
+
const moduleFederationAgentSkills = [
|
|
592
|
+
'mf'
|
|
593
|
+
];
|
|
594
|
+
const privateAgentSkills = [
|
|
595
|
+
'plan-graph',
|
|
596
|
+
'dag',
|
|
597
|
+
'subagent-graph',
|
|
598
|
+
'helm',
|
|
599
|
+
'debugger-mode'
|
|
600
|
+
];
|
|
601
|
+
const effectTsgoTypecheckCommand = "node -e \"const fs = require('node:fs'); const { execFileSync, spawnSync } = require('node:child_process'); const bin = execFileSync('effect-tsgo', ['get-exe-path'], { encoding: 'utf8' }).trim(); if (process.platform !== 'win32') fs.chmodSync(bin, 0o755); const result = spawnSync(bin, ['--noEmit', '-p', 'tsconfig.json'], { stdio: 'inherit' }); process.exit(result.status ?? 1);\"";
|
|
563
602
|
const modernPackageNames = [
|
|
564
603
|
'@modern-js/app-tools',
|
|
565
604
|
'@modern-js/plugin-bff',
|
|
605
|
+
'@modern-js/plugin-i18n',
|
|
566
606
|
'@modern-js/plugin-tanstack',
|
|
567
607
|
'@modern-js/runtime'
|
|
568
608
|
];
|
|
@@ -609,7 +649,15 @@ const remoteApps = [
|
|
|
609
649
|
mfName: 'remoteCommerce',
|
|
610
650
|
exposes: {
|
|
611
651
|
'./Route': './src/remote-entry.tsx',
|
|
612
|
-
'./Widget': './src/components/
|
|
652
|
+
'./Widget': './src/components/commerce-widget.tsx'
|
|
653
|
+
},
|
|
654
|
+
effectApi: {
|
|
655
|
+
stem: 'recommendations',
|
|
656
|
+
prefix: '/commerce-api',
|
|
657
|
+
consumedBy: [
|
|
658
|
+
shellApp.id,
|
|
659
|
+
'remote-commerce'
|
|
660
|
+
]
|
|
613
661
|
},
|
|
614
662
|
ownership: {
|
|
615
663
|
team: 'commerce-experience',
|
|
@@ -638,7 +686,15 @@ const remoteApps = [
|
|
|
638
686
|
mfName: 'remoteIdentity',
|
|
639
687
|
exposes: {
|
|
640
688
|
'./Route': './src/remote-entry.tsx',
|
|
641
|
-
'./Widget': './src/components/
|
|
689
|
+
'./Widget': './src/components/identity-widget.tsx'
|
|
690
|
+
},
|
|
691
|
+
effectApi: {
|
|
692
|
+
stem: 'identity',
|
|
693
|
+
prefix: '/identity-api',
|
|
694
|
+
consumedBy: [
|
|
695
|
+
shellApp.id,
|
|
696
|
+
'remote-identity'
|
|
697
|
+
]
|
|
642
698
|
},
|
|
643
699
|
ownership: {
|
|
644
700
|
team: 'identity-platform',
|
|
@@ -666,7 +722,7 @@ const remoteApps = [
|
|
|
666
722
|
port: 3023,
|
|
667
723
|
mfName: 'remoteDesignSystem',
|
|
668
724
|
exposes: {
|
|
669
|
-
'./Button': './src/components/
|
|
725
|
+
'./Button': './src/components/button.tsx',
|
|
670
726
|
'./tokens': './src/tokens.ts'
|
|
671
727
|
},
|
|
672
728
|
ownership: {
|
|
@@ -706,6 +762,81 @@ const effectService = {
|
|
|
706
762
|
}
|
|
707
763
|
}
|
|
708
764
|
};
|
|
765
|
+
const effectDiagnostics = [
|
|
766
|
+
'anyUnknownInErrorContext',
|
|
767
|
+
'classSelfMismatch',
|
|
768
|
+
'duplicatePackage',
|
|
769
|
+
'effectFnImplicitAny',
|
|
770
|
+
'floatingEffect',
|
|
771
|
+
'genericEffectServices',
|
|
772
|
+
'missingEffectContext',
|
|
773
|
+
'missingEffectError',
|
|
774
|
+
'missingLayerContext',
|
|
775
|
+
'missingReturnYieldStar',
|
|
776
|
+
'missingStarInYieldEffectGen',
|
|
777
|
+
'nonObjectEffectServiceType',
|
|
778
|
+
'outdatedApi',
|
|
779
|
+
'overriddenSchemaConstructor',
|
|
780
|
+
'catchUnfailableEffect',
|
|
781
|
+
'effectFnIife',
|
|
782
|
+
'effectGenUsesAdapter',
|
|
783
|
+
'effectInFailure',
|
|
784
|
+
'effectInVoidSuccess',
|
|
785
|
+
'globalErrorInEffectCatch',
|
|
786
|
+
'globalErrorInEffectFailure',
|
|
787
|
+
'layerMergeAllWithDependencies',
|
|
788
|
+
'lazyPromiseInEffectSync',
|
|
789
|
+
'leakingRequirements',
|
|
790
|
+
'multipleEffectProvide',
|
|
791
|
+
'returnEffectInGen',
|
|
792
|
+
'runEffectInsideEffect',
|
|
793
|
+
'schemaSyncInEffect',
|
|
794
|
+
'scopeInLayerEffect',
|
|
795
|
+
'strictEffectProvide',
|
|
796
|
+
'tryCatchInEffectGen',
|
|
797
|
+
'unknownInEffectCatch',
|
|
798
|
+
'asyncFunction',
|
|
799
|
+
'cryptoRandomUUID',
|
|
800
|
+
'cryptoRandomUUIDInEffect',
|
|
801
|
+
'extendsNativeError',
|
|
802
|
+
'globalConsole',
|
|
803
|
+
'globalConsoleInEffect',
|
|
804
|
+
'globalDate',
|
|
805
|
+
'globalDateInEffect',
|
|
806
|
+
'globalFetch',
|
|
807
|
+
'globalFetchInEffect',
|
|
808
|
+
'globalRandom',
|
|
809
|
+
'globalRandomInEffect',
|
|
810
|
+
'globalTimers',
|
|
811
|
+
'globalTimersInEffect',
|
|
812
|
+
'instanceOfSchema',
|
|
813
|
+
'newPromise',
|
|
814
|
+
'nodeBuiltinImport',
|
|
815
|
+
'preferSchemaOverJson',
|
|
816
|
+
'processEnv',
|
|
817
|
+
'processEnvInEffect',
|
|
818
|
+
'unsafeEffectTypeAssertion',
|
|
819
|
+
'catchAllToMapError',
|
|
820
|
+
'deterministicKeys',
|
|
821
|
+
'effectDoNotation',
|
|
822
|
+
'effectFnOpportunity',
|
|
823
|
+
'effectMapFlatten',
|
|
824
|
+
'effectMapVoid',
|
|
825
|
+
'effectSucceedWithVoid',
|
|
826
|
+
'missedPipeableOpportunity',
|
|
827
|
+
'missingEffectServiceDependency',
|
|
828
|
+
'nestedEffectGenYield',
|
|
829
|
+
'redundantSchemaTagIdentifier',
|
|
830
|
+
'schemaStructWithTag',
|
|
831
|
+
'schemaUnionOfLiterals',
|
|
832
|
+
'serviceNotAsClass',
|
|
833
|
+
'strictBooleanExpressions',
|
|
834
|
+
'unnecessaryArrowBlock',
|
|
835
|
+
'unnecessaryEffectGen',
|
|
836
|
+
'unnecessaryFailYieldableError',
|
|
837
|
+
'unnecessaryPipe',
|
|
838
|
+
'unnecessaryPipeChain'
|
|
839
|
+
];
|
|
709
840
|
const sharedPackages = [
|
|
710
841
|
{
|
|
711
842
|
id: 'shared-contracts',
|
|
@@ -723,6 +854,89 @@ const sharedPackages = [
|
|
|
723
854
|
description: 'Shared Effect API type placeholders for services and clients.'
|
|
724
855
|
}
|
|
725
856
|
];
|
|
857
|
+
function createNeutralOwnership(id, tier = 'tier-2-microvertical') {
|
|
858
|
+
return {
|
|
859
|
+
team: 'super-app-platform',
|
|
860
|
+
slack: '#super-app-platform',
|
|
861
|
+
pagerDuty: 'pd-super-app-platform',
|
|
862
|
+
runbookRef: `runbooks/microverticals/${id}.md`,
|
|
863
|
+
adrRef: `docs/super-app-rfc-adr/microverticals.md#${id}`,
|
|
864
|
+
blastRadius: {
|
|
865
|
+
tier,
|
|
866
|
+
references: [
|
|
867
|
+
`docs/super-app-rfc-adr/blast-radius.md#${id}`
|
|
868
|
+
]
|
|
869
|
+
}
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function createRemoteDescriptor(name, kind, port) {
|
|
873
|
+
const domain = toKebabCase(name);
|
|
874
|
+
const id = `remote-${domain}`;
|
|
875
|
+
const displayPrefix = toPascalCase(domain).replace(/([a-z])([A-Z])/g, '$1 $2');
|
|
876
|
+
return {
|
|
877
|
+
id,
|
|
878
|
+
directory: `apps/remotes/${id}`,
|
|
879
|
+
packageSuffix: id,
|
|
880
|
+
displayName: `${displayPrefix} Remote`,
|
|
881
|
+
kind: 'horizontal-remote' === kind ? 'horizontal-remote' : 'vertical',
|
|
882
|
+
domain,
|
|
883
|
+
portEnv: `REMOTE_${toEnvSegment(domain)}_PORT`,
|
|
884
|
+
port,
|
|
885
|
+
mfName: `remote${toPascalCase(domain)}`,
|
|
886
|
+
exposes: {
|
|
887
|
+
'./Route': './src/remote-entry.tsx',
|
|
888
|
+
'./Widget': `./src/components/${domain}-widget.tsx`
|
|
889
|
+
},
|
|
890
|
+
...'remote' === kind ? {
|
|
891
|
+
effectApi: {
|
|
892
|
+
stem: domain,
|
|
893
|
+
prefix: `/${domain}-api`,
|
|
894
|
+
consumedBy: [
|
|
895
|
+
shellApp.id,
|
|
896
|
+
id
|
|
897
|
+
]
|
|
898
|
+
}
|
|
899
|
+
} : {},
|
|
900
|
+
ownership: createNeutralOwnership(id)
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function createServiceDescriptor(name, port) {
|
|
904
|
+
const normalized = toKebabCase(name);
|
|
905
|
+
const suffix = normalized.endsWith('-effect') ? normalized : `service-${normalized.replace(/^service-/, '')}-effect`;
|
|
906
|
+
return {
|
|
907
|
+
id: suffix,
|
|
908
|
+
directory: `services/${suffix}`,
|
|
909
|
+
packageSuffix: suffix,
|
|
910
|
+
portEnv: `${toEnvSegment(suffix)}_PORT`,
|
|
911
|
+
port,
|
|
912
|
+
ownership: createNeutralOwnership(suffix, 'tier-2-effect-service')
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
function serviceApiPrefix(service) {
|
|
916
|
+
const name = service.id.replace(/^service-/, '').replace(/-effect$/, '');
|
|
917
|
+
return name.endsWith('-api') ? `/${name}` : `/${name}-api`;
|
|
918
|
+
}
|
|
919
|
+
function appHasEffectApi(app) {
|
|
920
|
+
return void 0 !== app.effectApi;
|
|
921
|
+
}
|
|
922
|
+
function effectApiPrefix(target) {
|
|
923
|
+
return target.effectApi?.prefix ?? serviceApiPrefix(target);
|
|
924
|
+
}
|
|
925
|
+
function effectApiStem(target) {
|
|
926
|
+
return target.effectApi?.stem ?? target.id.replace(/^service-/, '').replace(/-effect$/, '').replace(/-api$/, '');
|
|
927
|
+
}
|
|
928
|
+
function verticalEffectApps(remotes = remoteApps) {
|
|
929
|
+
return remotes.filter(appHasEffectApi);
|
|
930
|
+
}
|
|
931
|
+
function createSharedPackageDescriptor(name) {
|
|
932
|
+
const normalized = toKebabCase(name);
|
|
933
|
+
const id = normalized.startsWith('shared-') ? normalized : `shared-${normalized}`;
|
|
934
|
+
return {
|
|
935
|
+
id,
|
|
936
|
+
directory: `packages/${id}`,
|
|
937
|
+
description: `Shared ${normalized.replace(/^shared-/, '')} package placeholder.`
|
|
938
|
+
};
|
|
939
|
+
}
|
|
726
940
|
function normalizePath(filePath) {
|
|
727
941
|
return filePath.split(node_path.sep).join('/');
|
|
728
942
|
}
|
|
@@ -743,6 +957,15 @@ function writeFile(targetDir, relativePath, content) {
|
|
|
743
957
|
});
|
|
744
958
|
node_fs.writeFileSync(filePath, content, 'utf-8');
|
|
745
959
|
}
|
|
960
|
+
function writeFileReplacing(targetDir, relativePath, content) {
|
|
961
|
+
assertSafeRelativePath(relativePath);
|
|
962
|
+
const filePath = node_path.join(targetDir, relativePath);
|
|
963
|
+
ensureInsideRoot(targetDir, filePath);
|
|
964
|
+
node_fs.mkdirSync(node_path.dirname(filePath), {
|
|
965
|
+
recursive: true
|
|
966
|
+
});
|
|
967
|
+
node_fs.writeFileSync(filePath, content, 'utf-8');
|
|
968
|
+
}
|
|
746
969
|
function writeJson(targetDir, relativePath, value) {
|
|
747
970
|
writeFile(targetDir, relativePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
748
971
|
}
|
|
@@ -788,6 +1011,17 @@ function toPackageScope(packageName) {
|
|
|
788
1011
|
const normalized = packageName.replace(/^@/, '').replace(/[\\/]+/g, '-').toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[._-]+|[._-]+$/g, '').replace(/-{2,}/g, '-');
|
|
789
1012
|
return normalized || 'ultramodern-superapp';
|
|
790
1013
|
}
|
|
1014
|
+
function toKebabCase(value) {
|
|
1015
|
+
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, '');
|
|
1016
|
+
return normalized;
|
|
1017
|
+
}
|
|
1018
|
+
function toCamelCase(value) {
|
|
1019
|
+
const pascal = toPascalCase(value);
|
|
1020
|
+
return `${pascal.charAt(0).toLowerCase()}${pascal.slice(1)}`;
|
|
1021
|
+
}
|
|
1022
|
+
function toEnvSegment(value) {
|
|
1023
|
+
return toKebabCase(value).replace(/-/g, '_').toUpperCase();
|
|
1024
|
+
}
|
|
791
1025
|
function ultramodern_workspace_packageName(scope, suffix) {
|
|
792
1026
|
return `@${scope}/${suffix}`;
|
|
793
1027
|
}
|
|
@@ -818,26 +1052,44 @@ function modernPackageSpecifier(packageName, packageSource) {
|
|
|
818
1052
|
if (!packageSource.aliasScope) return packageSource.modernPackageVersion;
|
|
819
1053
|
return `npm:${modernAliasPackageName(packageName, packageSource)}@${packageSource.modernPackageVersion}`;
|
|
820
1054
|
}
|
|
821
|
-
function appDependencies(scope, packageSource) {
|
|
822
|
-
|
|
1055
|
+
function appDependencies(scope, packageSource, app) {
|
|
1056
|
+
const dependencies = {
|
|
823
1057
|
'@modern-js/plugin-tanstack': modernPackageSpecifier('@modern-js/plugin-tanstack', packageSource),
|
|
1058
|
+
'@modern-js/plugin-i18n': modernPackageSpecifier('@modern-js/plugin-i18n', packageSource),
|
|
824
1059
|
'@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
|
|
825
1060
|
'@module-federation/modern-js-v3': MODULE_FEDERATION_VERSION,
|
|
826
1061
|
'@module-federation/runtime': MODULE_FEDERATION_VERSION,
|
|
827
1062
|
'@tanstack/react-router': TANSTACK_ROUTER_VERSION,
|
|
1063
|
+
i18next: I18NEXT_VERSION,
|
|
1064
|
+
'node-fetch': '^3.3.2',
|
|
828
1065
|
[ultramodern_workspace_packageName(scope, 'shared-contracts')]: WORKSPACE_PACKAGE_VERSION,
|
|
829
1066
|
[ultramodern_workspace_packageName(scope, 'shared-design-tokens')]: WORKSPACE_PACKAGE_VERSION,
|
|
830
1067
|
react: REACT_VERSION,
|
|
831
1068
|
'react-dom': REACT_DOM_VERSION
|
|
832
1069
|
};
|
|
1070
|
+
if ('shell' === app.kind) {
|
|
1071
|
+
dependencies['@modern-js/plugin-bff'] = modernPackageSpecifier('@modern-js/plugin-bff', packageSource);
|
|
1072
|
+
for (const remote of verticalEffectApps())dependencies[ultramodern_workspace_packageName(scope, remote.packageSuffix)] = WORKSPACE_PACKAGE_VERSION;
|
|
1073
|
+
}
|
|
1074
|
+
if (appHasEffectApi(app)) dependencies['@modern-js/plugin-bff'] = modernPackageSpecifier('@modern-js/plugin-bff', packageSource);
|
|
1075
|
+
return dependencies;
|
|
833
1076
|
}
|
|
834
|
-
function appDevDependencies(packageSource) {
|
|
1077
|
+
function appDevDependencies(packageSource, enableTailwind) {
|
|
835
1078
|
return {
|
|
836
1079
|
'@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
|
|
1080
|
+
'@effect/tsgo': EFFECT_TSGO_VERSION,
|
|
1081
|
+
...enableTailwind ? {
|
|
1082
|
+
'@tailwindcss/postcss': `^${TAILWIND_POSTCSS_VERSION}`,
|
|
1083
|
+
postcss: '^8.5.6',
|
|
1084
|
+
tailwindcss: `^${TAILWIND_VERSION}`
|
|
1085
|
+
} : {},
|
|
1086
|
+
"@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
|
|
837
1087
|
'@types/node': '^20',
|
|
838
1088
|
'@types/react': '^19.1.8',
|
|
839
1089
|
'@types/react-dom': '^19.1.6',
|
|
840
|
-
typescript: TYPESCRIPT_VERSION
|
|
1090
|
+
typescript: TYPESCRIPT_VERSION,
|
|
1091
|
+
'zephyr-rspack-plugin': ZEPHYR_RSPACK_PLUGIN_VERSION,
|
|
1092
|
+
wrangler: WRANGLER_VERSION
|
|
841
1093
|
};
|
|
842
1094
|
}
|
|
843
1095
|
function createRootPackageJson(scope, packageSource) {
|
|
@@ -845,21 +1097,33 @@ function createRootPackageJson(scope, packageSource) {
|
|
|
845
1097
|
private: true,
|
|
846
1098
|
name: scope,
|
|
847
1099
|
version: '0.1.0',
|
|
1100
|
+
type: 'module',
|
|
1101
|
+
packageManager: `pnpm@${PNPM_VERSION}`,
|
|
848
1102
|
scripts: {
|
|
849
1103
|
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`,
|
|
850
1104
|
'dev:shell': `pnpm --filter ${ultramodern_workspace_packageName(scope, shellApp.packageSuffix)} dev`,
|
|
851
1105
|
'dev:commerce': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-commerce')} dev`,
|
|
852
1106
|
'dev:identity': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-identity')} dev`,
|
|
853
1107
|
'dev:design-system': `pnpm --filter ${ultramodern_workspace_packageName(scope, 'remote-design-system')} dev`,
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1108
|
+
build: 'pnpm -r --filter "./apps/remotes/**" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types',
|
|
1109
|
+
format: 'oxfmt .',
|
|
1110
|
+
'format:check': 'oxfmt --check .',
|
|
1111
|
+
lint: 'oxlint .',
|
|
1112
|
+
'lint:fix': 'oxlint . --fix',
|
|
1113
|
+
typecheck: `pnpm -r --filter "@${scope}/*" typecheck`,
|
|
1114
|
+
'cloudflare:build': 'pnpm -r --filter "./apps/remotes/**" run cloudflare:build && pnpm --filter "./apps/shell-super-app" run cloudflare:build && pnpm ultramodern:assert-mf-types',
|
|
1115
|
+
'skills:install': "node ./scripts/bootstrap-agent-skills.mjs",
|
|
1116
|
+
'skills:check': "node ./scripts/bootstrap-agent-skills.mjs --check",
|
|
1117
|
+
'agents:refs:install': "node ./scripts/setup-agent-reference-repos.mjs",
|
|
1118
|
+
'agents:refs:check': "node ./scripts/setup-agent-reference-repos.mjs --check",
|
|
1119
|
+
'ultramodern:assert-mf-types': "node ./scripts/assert-mf-types.mjs",
|
|
857
1120
|
'ultramodern:check': "node ./scripts/validate-ultramodern-workspace.mjs",
|
|
858
|
-
|
|
1121
|
+
postinstall: "node ./scripts/setup-agent-reference-repos.mjs && node ./scripts/bootstrap-agent-skills.mjs",
|
|
1122
|
+
check: 'pnpm format:check && pnpm lint && pnpm typecheck && pnpm skills:check && pnpm ultramodern:check'
|
|
859
1123
|
},
|
|
860
1124
|
engines: {
|
|
861
1125
|
node: '>=20',
|
|
862
|
-
pnpm:
|
|
1126
|
+
pnpm: `>=${PNPM_VERSION} <11.5.0`
|
|
863
1127
|
},
|
|
864
1128
|
workspaces: [
|
|
865
1129
|
'apps/*',
|
|
@@ -878,35 +1142,71 @@ function createRootPackageJson(scope, packageSource) {
|
|
|
878
1142
|
}
|
|
879
1143
|
},
|
|
880
1144
|
devDependencies: {
|
|
881
|
-
'@
|
|
882
|
-
typescript:
|
|
1145
|
+
'@effect/tsgo': EFFECT_TSGO_VERSION,
|
|
1146
|
+
"@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
|
|
1147
|
+
oxlint: OXLINT_VERSION,
|
|
1148
|
+
oxfmt: OXFMT_VERSION,
|
|
1149
|
+
ultracite: ULTRACITE_VERSION,
|
|
1150
|
+
wrangler: WRANGLER_VERSION,
|
|
1151
|
+
'zephyr-agent': ZEPHYR_AGENT_VERSION
|
|
883
1152
|
}
|
|
884
1153
|
};
|
|
885
1154
|
}
|
|
886
|
-
function
|
|
1155
|
+
function remoteDependencyAlias(remote) {
|
|
1156
|
+
return toCamelCase(remote.domain ?? remote.id.replace(/^remote-/, ''));
|
|
1157
|
+
}
|
|
1158
|
+
function zephyrRemoteDependency(scope, remote) {
|
|
1159
|
+
return `${ultramodern_workspace_packageName(scope, remote.packageSuffix)}@workspace:*`;
|
|
1160
|
+
}
|
|
1161
|
+
function createZephyrDependencies(scope, app, remotes = remoteApps) {
|
|
1162
|
+
if ('shell' !== app.kind) return {};
|
|
1163
|
+
return Object.fromEntries(remotes.map((remote)=>[
|
|
1164
|
+
remoteDependencyAlias(remote),
|
|
1165
|
+
zephyrRemoteDependency(scope, remote)
|
|
1166
|
+
]));
|
|
1167
|
+
}
|
|
1168
|
+
function createTsConfigBase() {
|
|
887
1169
|
return {
|
|
888
1170
|
compilerOptions: {
|
|
889
|
-
target: '
|
|
1171
|
+
target: 'ESNext',
|
|
890
1172
|
lib: [
|
|
1173
|
+
'ESNext',
|
|
891
1174
|
'DOM',
|
|
892
|
-
'DOM.Iterable'
|
|
893
|
-
'ES2022'
|
|
1175
|
+
'DOM.Iterable'
|
|
894
1176
|
],
|
|
895
|
-
module: '
|
|
1177
|
+
module: 'preserve',
|
|
896
1178
|
moduleResolution: 'Bundler',
|
|
1179
|
+
moduleDetection: 'force',
|
|
897
1180
|
jsx: 'preserve',
|
|
1181
|
+
isolatedModules: true,
|
|
1182
|
+
verbatimModuleSyntax: true,
|
|
898
1183
|
strict: true,
|
|
899
1184
|
noEmit: true,
|
|
1185
|
+
allowJs: true,
|
|
900
1186
|
esModuleInterop: true,
|
|
1187
|
+
noUncheckedIndexedAccess: true,
|
|
1188
|
+
exactOptionalPropertyTypes: true,
|
|
1189
|
+
noImplicitOverride: true,
|
|
1190
|
+
noFallthroughCasesInSwitch: true,
|
|
1191
|
+
noPropertyAccessFromIndexSignature: true,
|
|
1192
|
+
noImplicitReturns: true,
|
|
901
1193
|
skipLibCheck: true,
|
|
902
1194
|
resolveJsonModule: true,
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
1195
|
+
plugins: [
|
|
1196
|
+
{
|
|
1197
|
+
name: '@effect/language-service',
|
|
1198
|
+
diagnostics: true,
|
|
1199
|
+
includeSuggestionsInTsc: true,
|
|
1200
|
+
ignoreEffectSuggestionsInTscExitCode: false,
|
|
1201
|
+
ignoreEffectWarningsInTscExitCode: false,
|
|
1202
|
+
ignoreEffectErrorsInTscExitCode: false,
|
|
1203
|
+
skipDisabledOptimization: true,
|
|
1204
|
+
diagnosticSeverity: Object.fromEntries(effectDiagnostics.map((name)=>[
|
|
1205
|
+
name,
|
|
1206
|
+
'error'
|
|
1207
|
+
]))
|
|
1208
|
+
}
|
|
1209
|
+
]
|
|
910
1210
|
}
|
|
911
1211
|
};
|
|
912
1212
|
}
|
|
@@ -919,60 +1219,60 @@ function createPackageTsConfig(packageDir, includeApi = false) {
|
|
|
919
1219
|
if (includeApi) include.push('api', 'shared');
|
|
920
1220
|
return {
|
|
921
1221
|
extends: `${relativeRootFor(packageDir)}/tsconfig.base.json`,
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
'./src/*'
|
|
927
|
-
],
|
|
928
|
-
'@api/*': [
|
|
929
|
-
'./api/*'
|
|
930
|
-
],
|
|
931
|
-
'@shared/*': [
|
|
932
|
-
'./shared/*'
|
|
933
|
-
]
|
|
934
|
-
}
|
|
935
|
-
},
|
|
936
|
-
include
|
|
1222
|
+
include,
|
|
1223
|
+
exclude: [
|
|
1224
|
+
'src/modern-tanstack'
|
|
1225
|
+
]
|
|
937
1226
|
};
|
|
938
1227
|
}
|
|
939
|
-
function createAppPackage(scope, app, packageSource) {
|
|
940
|
-
|
|
1228
|
+
function createAppPackage(scope, app, packageSource, enableTailwind) {
|
|
1229
|
+
const packageJson = {
|
|
941
1230
|
private: true,
|
|
942
1231
|
name: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
943
1232
|
version: '0.1.0',
|
|
944
1233
|
scripts: {
|
|
945
1234
|
dev: 'modern dev',
|
|
946
|
-
build: 'modern build',
|
|
1235
|
+
build: app.exposes ? `modern build && node ${relativeRootFor(app.directory)}/scripts/assert-mf-types.mjs` : 'modern build',
|
|
1236
|
+
'cloudflare:build': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy',
|
|
1237
|
+
'cloudflare:preview': 'MODERNJS_DEPLOY=cloudflare modern build && MODERNJS_DEPLOY=cloudflare modern deploy && wrangler dev --config .output/wrangler.json',
|
|
947
1238
|
serve: 'modern serve',
|
|
948
|
-
typecheck:
|
|
1239
|
+
typecheck: effectTsgoTypecheckCommand
|
|
949
1240
|
},
|
|
950
1241
|
modernjs: {
|
|
951
1242
|
preset: 'presetUltramodern',
|
|
952
1243
|
role: 'shell' === app.kind ? 'shell' : 'module-federation-remote',
|
|
953
1244
|
appId: app.id,
|
|
954
|
-
topology: `${relativeRootFor(app.directory)}/topology/reference-topology.json
|
|
1245
|
+
topology: `${relativeRootFor(app.directory)}/topology/reference-topology.json`,
|
|
1246
|
+
...appHasEffectApi(app) ? {
|
|
1247
|
+
apiRuntime: 'effect-bff'
|
|
1248
|
+
} : {}
|
|
955
1249
|
},
|
|
956
|
-
dependencies:
|
|
957
|
-
|
|
1250
|
+
'zephyr:dependencies': createZephyrDependencies(scope, app),
|
|
1251
|
+
dependencies: appDependencies(scope, packageSource, app),
|
|
1252
|
+
devDependencies: appDevDependencies(packageSource, enableTailwind)
|
|
1253
|
+
};
|
|
1254
|
+
if (appHasEffectApi(app)) packageJson.exports = {
|
|
1255
|
+
'./effect/client': `./src/effect/${app.effectApi.stem}-client.ts`,
|
|
1256
|
+
'./shared/effect/api': './shared/effect/api.ts'
|
|
958
1257
|
};
|
|
1258
|
+
return packageJson;
|
|
959
1259
|
}
|
|
960
|
-
function createServicePackage(scope, packageSource) {
|
|
1260
|
+
function createServicePackage(scope, packageSource, enableTailwind, service = effectService) {
|
|
961
1261
|
return {
|
|
962
1262
|
private: true,
|
|
963
|
-
name: ultramodern_workspace_packageName(scope,
|
|
1263
|
+
name: ultramodern_workspace_packageName(scope, service.packageSuffix),
|
|
964
1264
|
version: '0.1.0',
|
|
965
1265
|
scripts: {
|
|
966
1266
|
dev: 'modern dev',
|
|
967
1267
|
build: 'modern build',
|
|
968
1268
|
serve: 'modern serve',
|
|
969
|
-
typecheck:
|
|
1269
|
+
typecheck: effectTsgoTypecheckCommand
|
|
970
1270
|
},
|
|
971
1271
|
modernjs: {
|
|
972
1272
|
preset: 'presetUltramodern',
|
|
973
1273
|
role: 'effect-service',
|
|
974
|
-
appId:
|
|
975
|
-
topology: `${relativeRootFor(
|
|
1274
|
+
appId: service.id,
|
|
1275
|
+
topology: `${relativeRootFor(service.directory)}/topology/reference-topology.json`
|
|
976
1276
|
},
|
|
977
1277
|
dependencies: {
|
|
978
1278
|
'@modern-js/runtime': modernPackageSpecifier('@modern-js/runtime', packageSource),
|
|
@@ -983,6 +1283,13 @@ function createServicePackage(scope, packageSource) {
|
|
|
983
1283
|
devDependencies: {
|
|
984
1284
|
'@modern-js/app-tools': modernPackageSpecifier('@modern-js/app-tools', packageSource),
|
|
985
1285
|
'@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource),
|
|
1286
|
+
'@effect/tsgo': EFFECT_TSGO_VERSION,
|
|
1287
|
+
...enableTailwind ? {
|
|
1288
|
+
'@tailwindcss/postcss': `^${TAILWIND_POSTCSS_VERSION}`,
|
|
1289
|
+
postcss: '^8.5.6',
|
|
1290
|
+
tailwindcss: `^${TAILWIND_VERSION}`
|
|
1291
|
+
} : {},
|
|
1292
|
+
"@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION,
|
|
986
1293
|
'@types/node': '^20',
|
|
987
1294
|
'@types/react': '^19.1.8',
|
|
988
1295
|
'@types/react-dom': '^19.1.6',
|
|
@@ -990,8 +1297,8 @@ function createServicePackage(scope, packageSource) {
|
|
|
990
1297
|
}
|
|
991
1298
|
};
|
|
992
1299
|
}
|
|
993
|
-
function createSharedPackage(scope, id, description) {
|
|
994
|
-
|
|
1300
|
+
function createSharedPackage(scope, id, description, packageSource) {
|
|
1301
|
+
const packageJson = {
|
|
995
1302
|
private: true,
|
|
996
1303
|
name: ultramodern_workspace_packageName(scope, id),
|
|
997
1304
|
version: '0.1.0',
|
|
@@ -1001,46 +1308,162 @@ function createSharedPackage(scope, id, description) {
|
|
|
1001
1308
|
'.': './src/index.ts'
|
|
1002
1309
|
},
|
|
1003
1310
|
scripts: {
|
|
1004
|
-
typecheck:
|
|
1311
|
+
typecheck: effectTsgoTypecheckCommand
|
|
1005
1312
|
},
|
|
1006
1313
|
devDependencies: {
|
|
1007
|
-
|
|
1314
|
+
'@effect/tsgo': EFFECT_TSGO_VERSION,
|
|
1315
|
+
"@typescript/native-preview": TYPESCRIPT_NATIVE_PREVIEW_VERSION
|
|
1008
1316
|
}
|
|
1009
1317
|
};
|
|
1318
|
+
if ('shared-effect-api' === id) packageJson.dependencies = {
|
|
1319
|
+
'@modern-js/plugin-bff': modernPackageSpecifier('@modern-js/plugin-bff', packageSource)
|
|
1320
|
+
};
|
|
1321
|
+
return packageJson;
|
|
1010
1322
|
}
|
|
1011
1323
|
function createAppModernConfig(app) {
|
|
1012
|
-
|
|
1324
|
+
const bffImport = appHasEffectApi(app) ? "import { bffPlugin } from '@modern-js/plugin-bff';\n" : '';
|
|
1325
|
+
const bffConfig = appHasEffectApi(app) ? ` bff: {
|
|
1326
|
+
effect: {
|
|
1327
|
+
openapi: {
|
|
1328
|
+
path: '/openapi.json',
|
|
1329
|
+
},
|
|
1330
|
+
},
|
|
1331
|
+
prefix: '${effectApiPrefix(app)}',
|
|
1332
|
+
runtimeFramework: 'effect',
|
|
1333
|
+
},
|
|
1334
|
+
` : '';
|
|
1335
|
+
const bffPluginEntry = appHasEffectApi(app) ? ' bffPlugin(),\n' : '';
|
|
1336
|
+
return `// @effect-diagnostics processEnv:off
|
|
1337
|
+
import {
|
|
1338
|
+
appTools,
|
|
1339
|
+
defineConfig,
|
|
1340
|
+
presetUltramodern,
|
|
1341
|
+
} from '@modern-js/app-tools';
|
|
1342
|
+
${bffImport}import { i18nPlugin } from '@modern-js/plugin-i18n';
|
|
1013
1343
|
import { tanstackRouterPlugin } from '@modern-js/plugin-tanstack';
|
|
1014
1344
|
import { moduleFederationPlugin } from '@module-federation/modern-js-v3';
|
|
1345
|
+
import { withZephyr as withZephyrRspack } from 'zephyr-rspack-plugin';
|
|
1346
|
+
|
|
1347
|
+
type ZephyrRspackConfig = Parameters<ReturnType<typeof withZephyrRspack>>[0];
|
|
1348
|
+
|
|
1349
|
+
const zephyrRspackPlugin = () => ({
|
|
1350
|
+
name: 'ultramodern-zephyr-rspack-plugin',
|
|
1351
|
+
pre: ['@modern-js/plugin-module-federation-config'],
|
|
1352
|
+
setup(api: {
|
|
1353
|
+
modifyRspackConfig: (
|
|
1354
|
+
handler: (
|
|
1355
|
+
config: ZephyrRspackConfig,
|
|
1356
|
+
) => ZephyrRspackConfig | Promise<ZephyrRspackConfig>,
|
|
1357
|
+
) => void;
|
|
1358
|
+
}) {
|
|
1359
|
+
api.modifyRspackConfig(config => withZephyrRspack()(config));
|
|
1360
|
+
},
|
|
1361
|
+
});
|
|
1015
1362
|
|
|
1016
1363
|
const appId = '${app.id}';
|
|
1017
|
-
const port = Number(process.env
|
|
1364
|
+
const port = Number(process.env['${app.portEnv}'] ?? ${app.port});
|
|
1365
|
+
const configuredSiteUrl = process.env['MODERN_PUBLIC_SITE_URL'];
|
|
1366
|
+
const hasConfiguredSiteUrl =
|
|
1367
|
+
typeof configuredSiteUrl === 'string' && configuredSiteUrl.length > 0;
|
|
1368
|
+
const isProductionBuild =
|
|
1369
|
+
process.env['NODE_ENV'] === 'production' || process.argv.includes('build');
|
|
1370
|
+
|
|
1371
|
+
if (isProductionBuild && !hasConfiguredSiteUrl) {
|
|
1372
|
+
throw new Error(
|
|
1373
|
+
'MODERN_PUBLIC_SITE_URL must be set for production builds so canonical and hreflang URLs use the deployed origin.',
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
const siteUrl = hasConfiguredSiteUrl
|
|
1378
|
+
? configuredSiteUrl
|
|
1379
|
+
: \`http://localhost:\${port}\`;
|
|
1018
1380
|
|
|
1019
1381
|
export default defineConfig(
|
|
1020
1382
|
presetUltramodern(
|
|
1021
1383
|
{
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
moduleFederationAppSSR: true,
|
|
1384
|
+
${bffConfig} output: {
|
|
1385
|
+
disableTsChecker: true,
|
|
1386
|
+
distPath: {
|
|
1387
|
+
html: './',
|
|
1027
1388
|
},
|
|
1028
|
-
},
|
|
1029
|
-
output: {
|
|
1030
1389
|
polyfill: 'off',
|
|
1031
|
-
disableTsChecker: true,
|
|
1032
1390
|
splitRouteChunks: false,
|
|
1033
1391
|
},
|
|
1392
|
+
performance: {
|
|
1393
|
+
rsdoctor: {
|
|
1394
|
+
enabled: process.env['ULTRAMODERN_RSDOCTOR'] === 'true',
|
|
1395
|
+
disableClientServer: true,
|
|
1396
|
+
},
|
|
1397
|
+
},
|
|
1398
|
+
html: {
|
|
1399
|
+
outputStructure: 'flat',
|
|
1400
|
+
},
|
|
1034
1401
|
plugins: [
|
|
1035
1402
|
appTools(),
|
|
1036
1403
|
tanstackRouterPlugin(),
|
|
1037
|
-
|
|
1404
|
+
i18nPlugin({
|
|
1405
|
+
backend: {
|
|
1406
|
+
enabled: true,
|
|
1407
|
+
},
|
|
1408
|
+
reactI18next: false,
|
|
1409
|
+
localeDetection: {
|
|
1410
|
+
fallbackLanguage: 'en',
|
|
1411
|
+
languages: ['en', 'cs'],
|
|
1412
|
+
localePathRedirect: true,
|
|
1413
|
+
ignoreRedirectRoutes: [
|
|
1414
|
+
'/@mf-types',
|
|
1415
|
+
'/bundles',
|
|
1416
|
+
'${effectApiPrefix(app)}',
|
|
1417
|
+
'/locales',
|
|
1418
|
+
'/mf-manifest.json',
|
|
1419
|
+
'/mf-stats.json',
|
|
1420
|
+
'/remoteEntry.js',
|
|
1421
|
+
'/static',
|
|
1422
|
+
'/zephyr-manifest.json',
|
|
1423
|
+
],
|
|
1424
|
+
},
|
|
1425
|
+
}),
|
|
1426
|
+
${bffPluginEntry} moduleFederationPlugin(),
|
|
1427
|
+
zephyrRspackPlugin(),
|
|
1038
1428
|
],
|
|
1429
|
+
tools: {
|
|
1430
|
+
autoprefixer: {
|
|
1431
|
+
overrideBrowserslist: ['defaults'],
|
|
1432
|
+
},
|
|
1433
|
+
bundlerChain: chain => {
|
|
1434
|
+
chain.ignoreWarnings([
|
|
1435
|
+
{
|
|
1436
|
+
message: /the request of a dependency is an expression/u,
|
|
1437
|
+
module: /modern-js-plugin-i18n/u,
|
|
1438
|
+
},
|
|
1439
|
+
]);
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
deploy: {
|
|
1443
|
+
target: 'cloudflare',
|
|
1444
|
+
worker: {
|
|
1445
|
+
ssr: true,
|
|
1446
|
+
},
|
|
1447
|
+
},
|
|
1448
|
+
server: {
|
|
1449
|
+
port,
|
|
1450
|
+
publicDir: './locales',
|
|
1451
|
+
ssr: {
|
|
1452
|
+
mode: 'stream',
|
|
1453
|
+
moduleFederationAppSSR: true,
|
|
1454
|
+
},
|
|
1455
|
+
},
|
|
1456
|
+
source: {
|
|
1457
|
+
mainEntryName: 'index',
|
|
1458
|
+
globalVars: {
|
|
1459
|
+
ULTRAMODERN_SITE_URL: siteUrl,
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1039
1462
|
},
|
|
1040
1463
|
{
|
|
1041
1464
|
appId,
|
|
1042
|
-
enableModuleFederationSSR: true,
|
|
1043
1465
|
enableBffRequestId: true,
|
|
1466
|
+
enableModuleFederationSSR: true,
|
|
1044
1467
|
enableTelemetryExporters: true,
|
|
1045
1468
|
telemetryFailLoudStartup: false,
|
|
1046
1469
|
},
|
|
@@ -1048,129 +1471,162 @@ export default defineConfig(
|
|
|
1048
1471
|
);
|
|
1049
1472
|
`;
|
|
1050
1473
|
}
|
|
1051
|
-
function
|
|
1052
|
-
return `
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
const require = createRequire(import.meta.url);
|
|
1057
|
-
const runtimeVersion = (
|
|
1058
|
-
require('@modern-js/runtime/package.json') as { version: string }
|
|
1059
|
-
).version;
|
|
1060
|
-
const reactVersion = (require('react/package.json') as { version: string })
|
|
1061
|
-
.version;
|
|
1062
|
-
const reactDomVersion = (
|
|
1063
|
-
require('react-dom/package.json') as { version: string }
|
|
1064
|
-
).version;
|
|
1065
|
-
|
|
1066
|
-
export default createModuleFederationConfig({
|
|
1067
|
-
name: '${shellApp.mfName}',
|
|
1068
|
-
dts: false,
|
|
1069
|
-
remotes: {
|
|
1070
|
-
commerce:
|
|
1071
|
-
process.env.REMOTE_COMMERCE_MF_MANIFEST ??
|
|
1072
|
-
'remoteCommerce@http://localhost:3021/mf-manifest.json',
|
|
1073
|
-
identity:
|
|
1074
|
-
process.env.REMOTE_IDENTITY_MF_MANIFEST ??
|
|
1075
|
-
'remoteIdentity@http://localhost:3022/mf-manifest.json',
|
|
1076
|
-
designSystem:
|
|
1077
|
-
process.env.REMOTE_DESIGN_SYSTEM_MF_MANIFEST ??
|
|
1078
|
-
'remoteDesignSystem@http://localhost:3023/mf-manifest.json',
|
|
1079
|
-
},
|
|
1080
|
-
shared: {
|
|
1081
|
-
react: {
|
|
1474
|
+
function createSharedModuleFederationConfig() {
|
|
1475
|
+
return ` shared: {
|
|
1476
|
+
'@modern-js/runtime': {
|
|
1477
|
+
requiredVersion: runtimeVersion,
|
|
1082
1478
|
singleton: true,
|
|
1083
|
-
requiredVersion: reactVersion,
|
|
1084
1479
|
treeShaking: false,
|
|
1085
1480
|
},
|
|
1086
|
-
'react-
|
|
1481
|
+
'@tanstack/react-router': {
|
|
1482
|
+
requiredVersion: dependencies['@tanstack/react-router'],
|
|
1087
1483
|
singleton: true,
|
|
1088
|
-
requiredVersion: reactDomVersion,
|
|
1089
1484
|
treeShaking: false,
|
|
1090
1485
|
},
|
|
1091
|
-
|
|
1486
|
+
react: {
|
|
1487
|
+
requiredVersion: reactVersion,
|
|
1092
1488
|
singleton: true,
|
|
1093
|
-
requiredVersion: dependencies['@tanstack/react-router'],
|
|
1094
1489
|
treeShaking: false,
|
|
1095
1490
|
},
|
|
1096
|
-
'
|
|
1491
|
+
'react-dom': {
|
|
1492
|
+
requiredVersion: reactDomVersion,
|
|
1097
1493
|
singleton: true,
|
|
1098
|
-
requiredVersion: runtimeVersion,
|
|
1099
1494
|
treeShaking: false,
|
|
1100
1495
|
},
|
|
1496
|
+
'react-dom/client': {
|
|
1497
|
+
requiredVersion: reactDomVersion,
|
|
1498
|
+
singleton: true,
|
|
1499
|
+
treeShaking: false,
|
|
1500
|
+
},
|
|
1501
|
+
}`;
|
|
1502
|
+
}
|
|
1503
|
+
function formatTsObjectLiteral(value) {
|
|
1504
|
+
const entries = Object.entries(value).sort(([left], [right])=>left.localeCompare(right));
|
|
1505
|
+
if (0 === entries.length) return '{}';
|
|
1506
|
+
return `{
|
|
1507
|
+
${entries.map(([key, entryValue])=>` '${key}': '${entryValue}',`).join('\n')}
|
|
1508
|
+
}`;
|
|
1509
|
+
}
|
|
1510
|
+
function createRemoteManifestEnv(remote) {
|
|
1511
|
+
return `REMOTE_${toEnvSegment(remote.domain ?? remote.id)}_MF_MANIFEST`;
|
|
1512
|
+
}
|
|
1513
|
+
function createShellModuleFederationConfig(remotes = remoteApps) {
|
|
1514
|
+
const remoteEntries = remotes.map((remote)=>{
|
|
1515
|
+
const key = remoteDependencyAlias(remote);
|
|
1516
|
+
return ` ${key}:
|
|
1517
|
+
process.env['${createRemoteManifestEnv(remote)}'] ??
|
|
1518
|
+
'${remote.mfName}@http://localhost:${remote.port}/mf-manifest.json',`;
|
|
1519
|
+
}).join('\n');
|
|
1520
|
+
return `// @effect-diagnostics nodeBuiltinImport:off processEnv:off
|
|
1521
|
+
import { createRequire } from 'node:module';
|
|
1522
|
+
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
|
|
1523
|
+
import { dependencies } from './package.json';
|
|
1524
|
+
|
|
1525
|
+
const require = createRequire(import.meta.url);
|
|
1526
|
+
const runtimeVersion = (require('@modern-js/runtime/package.json') as { version: string }).version;
|
|
1527
|
+
const reactVersion = (require('react/package.json') as { version: string }).version;
|
|
1528
|
+
const reactDomVersion = (require('react-dom/package.json') as { version: string }).version;
|
|
1529
|
+
|
|
1530
|
+
export default createModuleFederationConfig({
|
|
1531
|
+
treeShakingSharedExcludePlugins: ['RspackModuleFederationPlugin'],
|
|
1532
|
+
dev: {
|
|
1533
|
+
disableDynamicRemoteTypeHints: true,
|
|
1534
|
+
},
|
|
1535
|
+
dts: {
|
|
1536
|
+
displayErrorInTerminal: true,
|
|
1537
|
+
generateTypes: {
|
|
1538
|
+
compilerInstance: '--package typescript -- tsc',
|
|
1539
|
+
},
|
|
1101
1540
|
},
|
|
1541
|
+
filename: 'remoteEntry.js',
|
|
1542
|
+
name: '${shellApp.mfName}',
|
|
1543
|
+
remotes: {
|
|
1544
|
+
${remoteEntries}
|
|
1545
|
+
},
|
|
1546
|
+
${createSharedModuleFederationConfig()},
|
|
1102
1547
|
});
|
|
1103
1548
|
`;
|
|
1104
1549
|
}
|
|
1550
|
+
function createBuildMarker(scope, app) {
|
|
1551
|
+
return node_crypto.createHash('sha256').update(`${scope}:${app.packageSuffix}:${app.id}:0.1.0`).digest('hex').slice(0, 16);
|
|
1552
|
+
}
|
|
1553
|
+
function createUltramodernBuildModule(scope, app) {
|
|
1554
|
+
return `export const ultramodernVerticalIdentity = {
|
|
1555
|
+
appId: '${app.id}',
|
|
1556
|
+
packageName: '${ultramodern_workspace_packageName(scope, app.packageSuffix)}',
|
|
1557
|
+
version: '0.1.0',
|
|
1558
|
+
build: '${createBuildMarker(scope, app)}',
|
|
1559
|
+
deployProfile: 'cloudflare-ssr-mf-effect-v1',
|
|
1560
|
+
} as const;
|
|
1561
|
+
|
|
1562
|
+
export const ultramodernUiMarker = {
|
|
1563
|
+
...ultramodernVerticalIdentity,
|
|
1564
|
+
surface: 'ui',
|
|
1565
|
+
} as const;
|
|
1566
|
+
|
|
1567
|
+
export const ultramodernApiMarker = {
|
|
1568
|
+
...ultramodernVerticalIdentity,
|
|
1569
|
+
surface: 'effect-bff',
|
|
1570
|
+
} as const;
|
|
1571
|
+
`;
|
|
1572
|
+
}
|
|
1105
1573
|
function createRemoteModuleFederationConfig(app) {
|
|
1106
|
-
const exposes =
|
|
1107
|
-
return
|
|
1574
|
+
const exposes = formatTsObjectLiteral(app.exposes ?? {});
|
|
1575
|
+
return `// @effect-diagnostics nodeBuiltinImport:off
|
|
1576
|
+
import { createRequire } from 'node:module';
|
|
1108
1577
|
import { createModuleFederationConfig } from '@module-federation/modern-js-v3';
|
|
1109
1578
|
import { dependencies } from './package.json';
|
|
1110
1579
|
|
|
1111
1580
|
const require = createRequire(import.meta.url);
|
|
1112
|
-
const runtimeVersion = (
|
|
1113
|
-
|
|
1114
|
-
).version;
|
|
1115
|
-
const reactVersion = (require('react/package.json') as { version: string })
|
|
1116
|
-
.version;
|
|
1117
|
-
const reactDomVersion = (
|
|
1118
|
-
require('react-dom/package.json') as { version: string }
|
|
1119
|
-
).version;
|
|
1581
|
+
const runtimeVersion = (require('@modern-js/runtime/package.json') as { version: string }).version;
|
|
1582
|
+
const reactVersion = (require('react/package.json') as { version: string }).version;
|
|
1583
|
+
const reactDomVersion = (require('react-dom/package.json') as { version: string }).version;
|
|
1120
1584
|
|
|
1121
1585
|
export default createModuleFederationConfig({
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
treeShaking: false,
|
|
1131
|
-
},
|
|
1132
|
-
'react-dom': {
|
|
1133
|
-
singleton: true,
|
|
1134
|
-
requiredVersion: reactDomVersion,
|
|
1135
|
-
treeShaking: false,
|
|
1136
|
-
},
|
|
1137
|
-
'@tanstack/react-router': {
|
|
1138
|
-
singleton: true,
|
|
1139
|
-
requiredVersion: dependencies['@tanstack/react-router'],
|
|
1140
|
-
treeShaking: false,
|
|
1141
|
-
},
|
|
1142
|
-
'@modern-js/runtime': {
|
|
1143
|
-
singleton: true,
|
|
1144
|
-
requiredVersion: runtimeVersion,
|
|
1145
|
-
treeShaking: false,
|
|
1586
|
+
treeShakingSharedExcludePlugins: ['RspackModuleFederationPlugin'],
|
|
1587
|
+
dev: {
|
|
1588
|
+
disableDynamicRemoteTypeHints: true,
|
|
1589
|
+
},
|
|
1590
|
+
dts: {
|
|
1591
|
+
displayErrorInTerminal: true,
|
|
1592
|
+
generateTypes: {
|
|
1593
|
+
compilerInstance: '--package typescript -- tsc',
|
|
1146
1594
|
},
|
|
1147
1595
|
},
|
|
1596
|
+
exposes: ${exposes},
|
|
1597
|
+
filename: 'remoteEntry.js',
|
|
1598
|
+
name: '${app.mfName}',
|
|
1599
|
+
${createSharedModuleFederationConfig()},
|
|
1148
1600
|
});
|
|
1149
1601
|
`;
|
|
1150
1602
|
}
|
|
1151
|
-
function
|
|
1152
|
-
return
|
|
1603
|
+
function remoteWidgetFile(app) {
|
|
1604
|
+
return `${app.domain ?? app.id.replace(/^remote-/, '')}-widget`;
|
|
1605
|
+
}
|
|
1606
|
+
function createServiceModernConfigFor(service = effectService) {
|
|
1607
|
+
return `// @effect-diagnostics processEnv:off
|
|
1608
|
+
import { appTools, defineConfig, presetUltramodern } from '@modern-js/app-tools';
|
|
1153
1609
|
import { bffPlugin } from '@modern-js/plugin-bff';
|
|
1154
1610
|
|
|
1155
|
-
const appId = '${
|
|
1156
|
-
const port = Number(process.env
|
|
1611
|
+
const appId = '${service.id}';
|
|
1612
|
+
const port = Number(process.env['${service.portEnv}'] ?? ${service.port});
|
|
1157
1613
|
|
|
1158
1614
|
export default defineConfig(
|
|
1159
1615
|
presetUltramodern(
|
|
1160
1616
|
{
|
|
1161
|
-
server: {
|
|
1162
|
-
port,
|
|
1163
|
-
},
|
|
1164
1617
|
bff: {
|
|
1165
|
-
prefix: '/recommendations-api',
|
|
1166
|
-
runtimeFramework: 'effect',
|
|
1167
1618
|
effect: {
|
|
1168
1619
|
openapi: {
|
|
1169
1620
|
path: '/openapi.json',
|
|
1170
1621
|
},
|
|
1171
1622
|
},
|
|
1623
|
+
prefix: '${serviceApiPrefix(service)}',
|
|
1624
|
+
runtimeFramework: 'effect',
|
|
1172
1625
|
},
|
|
1173
1626
|
plugins: [appTools(), bffPlugin()],
|
|
1627
|
+
server: {
|
|
1628
|
+
port,
|
|
1629
|
+
},
|
|
1174
1630
|
},
|
|
1175
1631
|
{
|
|
1176
1632
|
appId,
|
|
@@ -1182,21 +1638,443 @@ export default defineConfig(
|
|
|
1182
1638
|
);
|
|
1183
1639
|
`;
|
|
1184
1640
|
}
|
|
1641
|
+
function createAppRuntimeConfig(app) {
|
|
1642
|
+
const resources = {
|
|
1643
|
+
cs: {
|
|
1644
|
+
translation: createAppLocaleMessages(app, 'cs')
|
|
1645
|
+
},
|
|
1646
|
+
en: {
|
|
1647
|
+
translation: createAppLocaleMessages(app, 'en')
|
|
1648
|
+
}
|
|
1649
|
+
};
|
|
1650
|
+
return `import { defineRuntimeConfig } from '@modern-js/runtime';
|
|
1651
|
+
import { createInstance } from 'i18next';
|
|
1652
|
+
|
|
1653
|
+
const i18nInstance = createInstance();
|
|
1654
|
+
|
|
1655
|
+
export default defineRuntimeConfig({
|
|
1656
|
+
i18n: {
|
|
1657
|
+
i18nInstance,
|
|
1658
|
+
initOptions: {
|
|
1659
|
+
defaultNS: 'translation',
|
|
1660
|
+
fallbackLng: 'en',
|
|
1661
|
+
interpolation: {
|
|
1662
|
+
escapeValue: false,
|
|
1663
|
+
},
|
|
1664
|
+
ns: ['translation'],
|
|
1665
|
+
resources: ${JSON.stringify(resources, null, 8).split('\n').join('\n ')},
|
|
1666
|
+
supportedLngs: ['en', 'cs'],
|
|
1667
|
+
},
|
|
1668
|
+
},
|
|
1669
|
+
router: {
|
|
1670
|
+
framework: 'tanstack',
|
|
1671
|
+
},
|
|
1672
|
+
});
|
|
1673
|
+
`;
|
|
1674
|
+
}
|
|
1675
|
+
function createAppStyles(enableTailwind) {
|
|
1676
|
+
return `${enableTailwind ? "@import 'tailwindcss';\n\n" : ''}:root {
|
|
1677
|
+
color: #10231c;
|
|
1678
|
+
background: #f1eadc;
|
|
1679
|
+
font-family:
|
|
1680
|
+
Geist,
|
|
1681
|
+
Inter,
|
|
1682
|
+
ui-sans-serif,
|
|
1683
|
+
system-ui,
|
|
1684
|
+
-apple-system,
|
|
1685
|
+
BlinkMacSystemFont,
|
|
1686
|
+
"Segoe UI",
|
|
1687
|
+
sans-serif;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
body {
|
|
1691
|
+
margin: 0;
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
main {
|
|
1695
|
+
min-height: 100vh;
|
|
1696
|
+
padding: 2rem;
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
nav {
|
|
1700
|
+
display: flex;
|
|
1701
|
+
gap: 0.75rem;
|
|
1702
|
+
margin-bottom: 2rem;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
a {
|
|
1706
|
+
color: #166b4b;
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
.commerce-shell {
|
|
1710
|
+
background: #f1eadc;
|
|
1711
|
+
color: #0b0a08;
|
|
1712
|
+
min-height: 100vh;
|
|
1713
|
+
padding: 1.5rem clamp(1rem, 4vw, 3rem) 4rem;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
.commerce-header,
|
|
1717
|
+
.commerce-footer {
|
|
1718
|
+
align-items: center;
|
|
1719
|
+
background: rgba(255, 255, 255, 0.86);
|
|
1720
|
+
box-shadow: 0 0.625rem 1.875rem rgba(25, 20, 12, 0.08);
|
|
1721
|
+
display: flex;
|
|
1722
|
+
gap: 1.25rem;
|
|
1723
|
+
justify-content: space-between;
|
|
1724
|
+
margin: 0 auto;
|
|
1725
|
+
max-width: 88rem;
|
|
1726
|
+
padding: 1.25rem 1.75rem;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
.commerce-logo {
|
|
1730
|
+
font-size: 1.35rem;
|
|
1731
|
+
font-weight: 800;
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
.commerce-nav,
|
|
1735
|
+
.commerce-actions,
|
|
1736
|
+
.commerce-language {
|
|
1737
|
+
align-items: center;
|
|
1738
|
+
display: flex;
|
|
1739
|
+
flex-wrap: wrap;
|
|
1740
|
+
gap: 0.75rem;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
.commerce-nav {
|
|
1744
|
+
margin: 0;
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
.commerce-pill,
|
|
1748
|
+
.commerce-button,
|
|
1749
|
+
.commerce-link-button,
|
|
1750
|
+
.commerce-cart-button,
|
|
1751
|
+
.commerce-quantity-button {
|
|
1752
|
+
align-items: center;
|
|
1753
|
+
border-radius: 999px;
|
|
1754
|
+
border: 0.0625rem solid rgba(23, 23, 23, 0.14);
|
|
1755
|
+
box-shadow: 0 0.25rem 0.75rem rgba(20, 17, 10, 0.08);
|
|
1756
|
+
color: #14120d;
|
|
1757
|
+
display: inline-flex;
|
|
1758
|
+
font: inherit;
|
|
1759
|
+
font-weight: 750;
|
|
1760
|
+
justify-content: center;
|
|
1761
|
+
min-height: 2.5rem;
|
|
1762
|
+
padding: 0.65rem 1.05rem;
|
|
1763
|
+
text-decoration: none;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
.commerce-button {
|
|
1767
|
+
background: #00624b;
|
|
1768
|
+
border-color: #00624b;
|
|
1769
|
+
color: #ffffff;
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
.commerce-link-button,
|
|
1773
|
+
.commerce-pill,
|
|
1774
|
+
.commerce-cart-button,
|
|
1775
|
+
.commerce-quantity-button {
|
|
1776
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
.commerce-page {
|
|
1780
|
+
margin: 3rem auto 0;
|
|
1781
|
+
max-width: 88rem;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
.commerce-product {
|
|
1785
|
+
align-items: center;
|
|
1786
|
+
display: grid;
|
|
1787
|
+
gap: clamp(2rem, 5vw, 4rem);
|
|
1788
|
+
grid-template-columns: minmax(0, 1fr) minmax(20rem, 0.95fr);
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.commerce-product-media {
|
|
1792
|
+
aspect-ratio: 1 / 0.92;
|
|
1793
|
+
background:
|
|
1794
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0) 42%),
|
|
1795
|
+
linear-gradient(135deg, #97c66d 0 20%, #6f9748 20% 34%, #d6c15d 34% 36%, #6a8f3e 36% 46%, #315824 46% 64%, #8bb85e 64% 100%);
|
|
1796
|
+
border: 1.25rem solid #ffe987;
|
|
1797
|
+
border-radius: 1.6rem;
|
|
1798
|
+
box-shadow: inset 0 -7rem 8rem rgba(58, 77, 35, 0.22);
|
|
1799
|
+
overflow: hidden;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
.commerce-product-media::after {
|
|
1803
|
+
background:
|
|
1804
|
+
radial-gradient(circle at 27% 76%, #1e2422 0 5%, transparent 5.4%),
|
|
1805
|
+
radial-gradient(circle at 55% 76%, #1e2422 0 6%, transparent 6.4%),
|
|
1806
|
+
linear-gradient(0deg, #004b7b 0 100%);
|
|
1807
|
+
border-radius: 1.2rem;
|
|
1808
|
+
content: "";
|
|
1809
|
+
display: block;
|
|
1810
|
+
height: 19%;
|
|
1811
|
+
margin: 58% auto 0;
|
|
1812
|
+
width: 42%;
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
.commerce-eyebrow {
|
|
1816
|
+
color: #00624b;
|
|
1817
|
+
font-size: 0.85rem;
|
|
1818
|
+
font-weight: 850;
|
|
1819
|
+
letter-spacing: 0.16rem;
|
|
1820
|
+
text-transform: uppercase;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
.commerce-title {
|
|
1824
|
+
font-size: clamp(2.5rem, 6vw, 4.8rem);
|
|
1825
|
+
line-height: 0.95;
|
|
1826
|
+
margin: 0.65rem 0 1.4rem;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
.commerce-lede {
|
|
1830
|
+
color: #555149;
|
|
1831
|
+
font-size: 1.2rem;
|
|
1832
|
+
line-height: 1.65;
|
|
1833
|
+
max-width: 42rem;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
.commerce-facts {
|
|
1837
|
+
display: grid;
|
|
1838
|
+
gap: 1rem;
|
|
1839
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
1840
|
+
margin: 2rem 0;
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
.commerce-fact,
|
|
1844
|
+
.commerce-card,
|
|
1845
|
+
.commerce-cart-panel {
|
|
1846
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1847
|
+
border-radius: 1rem;
|
|
1848
|
+
box-shadow: 0 0.5rem 1.25rem rgba(25, 20, 12, 0.08);
|
|
1849
|
+
padding: 1.25rem;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
.commerce-fact span,
|
|
1853
|
+
.commerce-card span {
|
|
1854
|
+
color: #767067;
|
|
1855
|
+
display: block;
|
|
1856
|
+
font-weight: 750;
|
|
1857
|
+
margin-bottom: 0.45rem;
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
.commerce-fact strong {
|
|
1861
|
+
font-size: 1.1rem;
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
.commerce-checkout {
|
|
1865
|
+
align-items: center;
|
|
1866
|
+
display: flex;
|
|
1867
|
+
flex-wrap: wrap;
|
|
1868
|
+
gap: 0.75rem;
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
.commerce-section-title {
|
|
1872
|
+
font-size: 1.8rem;
|
|
1873
|
+
margin: 4.5rem 0 1.5rem;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
.commerce-grid {
|
|
1877
|
+
display: grid;
|
|
1878
|
+
gap: 1rem;
|
|
1879
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
.commerce-card strong {
|
|
1883
|
+
display: block;
|
|
1884
|
+
font-size: 1.45rem;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
.commerce-cart-panel {
|
|
1888
|
+
margin-top: 2rem;
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
.commerce-cart-line {
|
|
1892
|
+
align-items: center;
|
|
1893
|
+
border-top: 0.0625rem solid rgba(23, 23, 23, 0.12);
|
|
1894
|
+
display: grid;
|
|
1895
|
+
gap: 1rem;
|
|
1896
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
1897
|
+
padding: 1rem 0;
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
.commerce-cart-line:first-of-type {
|
|
1901
|
+
border-top: 0;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
.commerce-quantity {
|
|
1905
|
+
align-items: center;
|
|
1906
|
+
display: flex;
|
|
1907
|
+
gap: 0.45rem;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
.commerce-quantity-button {
|
|
1911
|
+
min-height: 2rem;
|
|
1912
|
+
min-width: 2rem;
|
|
1913
|
+
padding: 0.25rem;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
.commerce-boundary-toggle {
|
|
1917
|
+
align-items: center;
|
|
1918
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1919
|
+
border-radius: 0.8rem;
|
|
1920
|
+
bottom: 1.5rem;
|
|
1921
|
+
box-shadow: 0 0.75rem 2rem rgba(18, 15, 10, 0.14);
|
|
1922
|
+
display: flex;
|
|
1923
|
+
gap: 0.65rem;
|
|
1924
|
+
left: 1.5rem;
|
|
1925
|
+
padding: 0.8rem 1rem;
|
|
1926
|
+
position: fixed;
|
|
1927
|
+
z-index: 80;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
.commerce-boundary-toggle input {
|
|
1931
|
+
accent-color: #00624b;
|
|
1932
|
+
height: 1rem;
|
|
1933
|
+
width: 1rem;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
.boundary-overlay {
|
|
1937
|
+
inset: 0;
|
|
1938
|
+
pointer-events: none;
|
|
1939
|
+
position: fixed;
|
|
1940
|
+
z-index: 70;
|
|
1941
|
+
}
|
|
1942
|
+
|
|
1943
|
+
.boundary-overlay__box {
|
|
1944
|
+
border: 0.0625rem solid var(--boundary-color);
|
|
1945
|
+
border-radius: 0.55rem;
|
|
1946
|
+
box-shadow:
|
|
1947
|
+
0 0 0 0.0625rem rgba(255, 255, 255, 0.72),
|
|
1948
|
+
0 0.35rem 1.25rem color-mix(in srgb, var(--boundary-color) 20%, transparent);
|
|
1949
|
+
position: fixed;
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
.boundary-overlay__label {
|
|
1953
|
+
background: color-mix(in srgb, var(--boundary-color) 88%, white);
|
|
1954
|
+
border-radius: 999px;
|
|
1955
|
+
color: #0b0a08;
|
|
1956
|
+
font-size: 0.7rem;
|
|
1957
|
+
font-weight: 850;
|
|
1958
|
+
line-height: 1;
|
|
1959
|
+
padding: 0.3rem 0.55rem;
|
|
1960
|
+
position: absolute;
|
|
1961
|
+
right: 0.35rem;
|
|
1962
|
+
top: 0.35rem;
|
|
1963
|
+
white-space: nowrap;
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
.boundary-overlay__box[data-label-placement="above"] .boundary-overlay__label {
|
|
1967
|
+
bottom: calc(100% + 0.25rem);
|
|
1968
|
+
top: auto;
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1971
|
+
@media (max-width: 860px) {
|
|
1972
|
+
.commerce-header,
|
|
1973
|
+
.commerce-footer,
|
|
1974
|
+
.commerce-product,
|
|
1975
|
+
.commerce-grid,
|
|
1976
|
+
.commerce-facts {
|
|
1977
|
+
grid-template-columns: 1fr;
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
.commerce-header,
|
|
1981
|
+
.commerce-footer {
|
|
1982
|
+
align-items: flex-start;
|
|
1983
|
+
flex-direction: column;
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1986
|
+
.commerce-product-media {
|
|
1987
|
+
min-height: 20rem;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
`;
|
|
1991
|
+
}
|
|
1992
|
+
function createPostcssConfig() {
|
|
1993
|
+
return `export default {
|
|
1994
|
+
plugins: {
|
|
1995
|
+
'@tailwindcss/postcss': {},
|
|
1996
|
+
},
|
|
1997
|
+
};
|
|
1998
|
+
`;
|
|
1999
|
+
}
|
|
2000
|
+
function createTailwindConfig() {
|
|
2001
|
+
return `import type { Config } from 'tailwindcss';
|
|
2002
|
+
|
|
2003
|
+
export default {
|
|
2004
|
+
content: ['./src/**/*.{js,jsx,ts,tsx}'],
|
|
2005
|
+
} satisfies Config;
|
|
2006
|
+
`;
|
|
2007
|
+
}
|
|
2008
|
+
function createLocalizedHeadComponent() {
|
|
2009
|
+
return `const fallbackLanguage = 'en';
|
|
2010
|
+
const supportedLanguages = ['en', 'cs'] as const;
|
|
2011
|
+
type SupportedLanguage = (typeof supportedLanguages)[number];
|
|
2012
|
+
|
|
2013
|
+
const localizedPath = (language: SupportedLanguage) => \`/\${language}\`;
|
|
2014
|
+
|
|
2015
|
+
const absoluteUrl = (pathname: string) => {
|
|
2016
|
+
const origin = ULTRAMODERN_SITE_URL.replace(/\\/+$/u, '');
|
|
2017
|
+
return \`\${origin}\${pathname}\`;
|
|
2018
|
+
};
|
|
2019
|
+
const LocalizedHead = () => {
|
|
2020
|
+
const canonicalPath = localizedPath(fallbackLanguage);
|
|
2021
|
+
|
|
2022
|
+
return (
|
|
2023
|
+
<>
|
|
2024
|
+
<link rel="canonical" href={absoluteUrl(canonicalPath)} />
|
|
2025
|
+
{supportedLanguages.map(code => (
|
|
2026
|
+
<link
|
|
2027
|
+
href={absoluteUrl(localizedPath(code))}
|
|
2028
|
+
hrefLang={code}
|
|
2029
|
+
key={code}
|
|
2030
|
+
rel="alternate"
|
|
2031
|
+
/>
|
|
2032
|
+
))}
|
|
2033
|
+
<link
|
|
2034
|
+
href={absoluteUrl(localizedPath(fallbackLanguage))}
|
|
2035
|
+
hrefLang="x-default"
|
|
2036
|
+
rel="alternate"
|
|
2037
|
+
/>
|
|
2038
|
+
</>
|
|
2039
|
+
);
|
|
2040
|
+
};
|
|
2041
|
+
`;
|
|
2042
|
+
}
|
|
1185
2043
|
function createShellPage() {
|
|
1186
|
-
return `
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
];
|
|
2044
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2045
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2046
|
+
|
|
2047
|
+
const languageCodes = ['en', 'cs'] as const;
|
|
1191
2048
|
|
|
2049
|
+
const remoteKeys = ['commerce', 'identity', 'designSystem'] as const;
|
|
2050
|
+
|
|
2051
|
+
${createLocalizedHeadComponent()}
|
|
1192
2052
|
export default function ShellHome() {
|
|
2053
|
+
const { i18nInstance, language } = useModernI18n();
|
|
2054
|
+
const t = i18nInstance.t.bind(i18nInstance);
|
|
2055
|
+
|
|
1193
2056
|
return (
|
|
1194
2057
|
<main>
|
|
1195
|
-
<
|
|
2058
|
+
<LocalizedHead />
|
|
2059
|
+
<nav aria-label={t('shell.language.switcher')}>
|
|
2060
|
+
{languageCodes.map(code => (
|
|
2061
|
+
<a
|
|
2062
|
+
aria-current={language === code ? 'page' : undefined}
|
|
2063
|
+
href={\`/\${code}\`}
|
|
2064
|
+
key={code}
|
|
2065
|
+
>
|
|
2066
|
+
{t(\`shell.language.\${code}\`)}
|
|
2067
|
+
</a>
|
|
2068
|
+
))}
|
|
2069
|
+
</nav>
|
|
2070
|
+
<h1>{t('shell.title')}</h1>
|
|
1196
2071
|
<p data-testid="ultramodern-preset">presetUltramodern workspace</p>
|
|
2072
|
+
<p data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
2073
|
+
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
2074
|
+
</p>
|
|
1197
2075
|
<ul>
|
|
1198
|
-
{
|
|
1199
|
-
<li key={remote}>{remote}</li>
|
|
2076
|
+
{remoteKeys.map(remote => (
|
|
2077
|
+
<li key={remote}>{t(\`shell.remotes.\${remote}\`)}</li>
|
|
1200
2078
|
))}
|
|
1201
2079
|
</ul>
|
|
1202
2080
|
</main>
|
|
@@ -1205,45 +2083,603 @@ export default function ShellHome() {
|
|
|
1205
2083
|
`;
|
|
1206
2084
|
}
|
|
1207
2085
|
function createRemotePage(app) {
|
|
1208
|
-
|
|
1209
|
-
|
|
2086
|
+
if ('remote-commerce' === app.id) return createCommerceRemotePage(app);
|
|
2087
|
+
const effectBffImport = appHasEffectApi(app) ? `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2088
|
+
import { useEffect, useState } from 'react';
|
|
2089
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2090
|
+
` : "import { useModernI18n } from '@modern-js/plugin-i18n/runtime';\nimport { ultramodernUiMarker } from '../../ultramodern-build';\n";
|
|
2091
|
+
const effectBffState = appHasEffectApi(app) ? ` const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2092
|
+
|
|
2093
|
+
useEffect(() => {
|
|
2094
|
+
void fetch('${effectApiPrefix(app)}/effect/${effectApiStem(app)}?limit=1', {
|
|
2095
|
+
headers: {
|
|
2096
|
+
accept: 'application/json',
|
|
2097
|
+
},
|
|
2098
|
+
})
|
|
2099
|
+
.then(response => {
|
|
2100
|
+
if (!response.ok) {
|
|
2101
|
+
throw new Error(\`Effect BFF request failed: \${response.status}\`);
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
return response.json() as Promise<{ items?: Array<{ title?: string }> }>;
|
|
2105
|
+
})
|
|
2106
|
+
.then(data => {
|
|
2107
|
+
setEffectApiStatus(data.items[0]?.title ?? 'empty');
|
|
2108
|
+
})
|
|
2109
|
+
.catch(() => {
|
|
2110
|
+
setEffectApiStatus('unavailable');
|
|
2111
|
+
});
|
|
2112
|
+
}, []);
|
|
2113
|
+
|
|
2114
|
+
` : '';
|
|
2115
|
+
const effectBffMarkup = appHasEffectApi(app) ? ` <p data-testid="effect-bff-status">{effectApiStatus}</p>
|
|
2116
|
+
` : '';
|
|
2117
|
+
return `${effectBffImport}
|
|
2118
|
+
${createLocalizedHeadComponent()}
|
|
2119
|
+
export default function ${toPascalCase(app.id)}Home() {
|
|
2120
|
+
const { i18nInstance, language } = useModernI18n();
|
|
2121
|
+
const t = i18nInstance.t.bind(i18nInstance);
|
|
2122
|
+
${effectBffState} return (
|
|
1210
2123
|
<main>
|
|
1211
|
-
<
|
|
1212
|
-
<
|
|
2124
|
+
<LocalizedHead />
|
|
2125
|
+
<nav aria-label={t('${app.domain}.language.switcher')}>
|
|
2126
|
+
{supportedLanguages.map(code => (
|
|
2127
|
+
<a
|
|
2128
|
+
aria-current={language === code ? 'page' : undefined}
|
|
2129
|
+
href={\`/\${code}\`}
|
|
2130
|
+
key={code}
|
|
2131
|
+
>
|
|
2132
|
+
{t(\`${app.domain}.language.\${code}\`)}
|
|
2133
|
+
</a>
|
|
2134
|
+
))}
|
|
2135
|
+
</nav>
|
|
2136
|
+
<h1>{t('${app.domain}.title')}</h1>
|
|
2137
|
+
<p data-mf-role="${app.kind}">{t('${app.domain}.role')}</p>
|
|
2138
|
+
<p data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
2139
|
+
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
2140
|
+
</p>
|
|
2141
|
+
${effectBffMarkup} </main>
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
`;
|
|
2145
|
+
}
|
|
2146
|
+
function createCommerceRemotePage(app) {
|
|
2147
|
+
return `import { useModernI18n } from '@modern-js/plugin-i18n/runtime';
|
|
2148
|
+
import { useEffect, useState, type CSSProperties } from 'react';
|
|
2149
|
+
import { ultramodernUiMarker } from '../../ultramodern-build';
|
|
2150
|
+
|
|
2151
|
+
const languageCodes = ['en', 'cs'] as const;
|
|
2152
|
+
|
|
2153
|
+
const boundaryDefinitions = [
|
|
2154
|
+
{
|
|
2155
|
+
color: '#ff5a57',
|
|
2156
|
+
id: 'explore',
|
|
2157
|
+
labelKey: 'commerce.boundaries.explore',
|
|
2158
|
+
},
|
|
2159
|
+
{
|
|
2160
|
+
color: '#24d671',
|
|
2161
|
+
id: 'decide',
|
|
2162
|
+
labelKey: 'commerce.boundaries.decide',
|
|
2163
|
+
},
|
|
2164
|
+
{
|
|
2165
|
+
color: '#f4d044',
|
|
2166
|
+
id: 'checkout',
|
|
2167
|
+
labelKey: 'commerce.boundaries.checkout',
|
|
2168
|
+
},
|
|
2169
|
+
] as const;
|
|
2170
|
+
|
|
2171
|
+
type BoundaryId = (typeof boundaryDefinitions)[number]['id'];
|
|
2172
|
+
type BoundaryDefinition = (typeof boundaryDefinitions)[number];
|
|
2173
|
+
|
|
2174
|
+
const boundaryMetadata: Record<BoundaryId, BoundaryDefinition> = {
|
|
2175
|
+
checkout: boundaryDefinitions[2],
|
|
2176
|
+
decide: boundaryDefinitions[1],
|
|
2177
|
+
explore: boundaryDefinitions[0],
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
const products = [
|
|
2181
|
+
{
|
|
2182
|
+
id: 'field-loader-112',
|
|
2183
|
+
titleKey: 'commerce.products.fieldLoader.title',
|
|
2184
|
+
descriptionKey: 'commerce.products.fieldLoader.description',
|
|
2185
|
+
priceKey: 'commerce.products.fieldLoader.price',
|
|
2186
|
+
powerKey: 'commerce.products.fieldLoader.power',
|
|
2187
|
+
availabilityKey: 'commerce.products.fieldLoader.availability',
|
|
2188
|
+
},
|
|
2189
|
+
{
|
|
2190
|
+
id: 'orchard-tractor',
|
|
2191
|
+
titleKey: 'commerce.products.orchard.title',
|
|
2192
|
+
badgeKey: 'commerce.products.orchard.badge',
|
|
2193
|
+
},
|
|
2194
|
+
{
|
|
2195
|
+
id: 'autonomy-kit',
|
|
2196
|
+
titleKey: 'commerce.products.autonomy.title',
|
|
2197
|
+
badgeKey: 'commerce.products.autonomy.badge',
|
|
2198
|
+
},
|
|
2199
|
+
] as const;
|
|
2200
|
+
|
|
2201
|
+
type ProductId = (typeof products)[number]['id'];
|
|
2202
|
+
type CartState = Partial<Record<ProductId, number>>;
|
|
2203
|
+
|
|
2204
|
+
type BoundaryLabels = Record<BoundaryId, string>;
|
|
2205
|
+
type BoundaryBox = {
|
|
2206
|
+
color: string;
|
|
2207
|
+
height: number;
|
|
2208
|
+
id: BoundaryId;
|
|
2209
|
+
label: string;
|
|
2210
|
+
labelPlacement: 'above' | 'inside';
|
|
2211
|
+
left: number;
|
|
2212
|
+
top: number;
|
|
2213
|
+
width: number;
|
|
2214
|
+
};
|
|
2215
|
+
|
|
2216
|
+
const featuredProduct = products[0];
|
|
2217
|
+
const recommendations = [products[1], products[2]] as const;
|
|
2218
|
+
|
|
2219
|
+
${createLocalizedHeadComponent()}
|
|
2220
|
+
const isBoundaryId = (value: string): value is BoundaryId =>
|
|
2221
|
+
Object.prototype.hasOwnProperty.call(boundaryMetadata, value);
|
|
2222
|
+
|
|
2223
|
+
function collectBoundaryBoxes(labels: BoundaryLabels): BoundaryBox[] {
|
|
2224
|
+
return Array.from(
|
|
2225
|
+
document.querySelectorAll<HTMLElement>('[data-boundary], [data-boundary-page]'),
|
|
2226
|
+
)
|
|
2227
|
+
.map(element => {
|
|
2228
|
+
const id = element.dataset.boundary ?? element.dataset.boundaryPage;
|
|
2229
|
+
|
|
2230
|
+
if (id === undefined || !isBoundaryId(id)) {
|
|
2231
|
+
return undefined;
|
|
2232
|
+
}
|
|
2233
|
+
|
|
2234
|
+
const rect = element.getBoundingClientRect();
|
|
2235
|
+
|
|
2236
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
2237
|
+
return undefined;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
return {
|
|
2241
|
+
color: boundaryMetadata[id].color,
|
|
2242
|
+
height: rect.height,
|
|
2243
|
+
id,
|
|
2244
|
+
label: labels[id],
|
|
2245
|
+
labelPlacement: rect.top > 28 ? 'above' : 'inside',
|
|
2246
|
+
left: rect.left,
|
|
2247
|
+
top: rect.top,
|
|
2248
|
+
width: rect.width,
|
|
2249
|
+
};
|
|
2250
|
+
})
|
|
2251
|
+
.filter((box): box is BoundaryBox => box !== undefined);
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
function BoundaryOverlay({
|
|
2255
|
+
labels,
|
|
2256
|
+
visible,
|
|
2257
|
+
}: {
|
|
2258
|
+
labels: BoundaryLabels;
|
|
2259
|
+
visible: boolean;
|
|
2260
|
+
}) {
|
|
2261
|
+
const [boxes, setBoxes] = useState<BoundaryBox[]>([]);
|
|
2262
|
+
|
|
2263
|
+
useEffect(() => {
|
|
2264
|
+
if (!visible) {
|
|
2265
|
+
setBoxes([]);
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
let animationFrame = 0;
|
|
2270
|
+
const update = () => {
|
|
2271
|
+
cancelAnimationFrame(animationFrame);
|
|
2272
|
+
animationFrame = requestAnimationFrame(() => {
|
|
2273
|
+
setBoxes(collectBoundaryBoxes(labels));
|
|
2274
|
+
});
|
|
2275
|
+
};
|
|
2276
|
+
const observer = new ResizeObserver(update);
|
|
2277
|
+
|
|
2278
|
+
observer.observe(document.body);
|
|
2279
|
+
update();
|
|
2280
|
+
window.addEventListener('resize', update);
|
|
2281
|
+
window.addEventListener('scroll', update, true);
|
|
2282
|
+
|
|
2283
|
+
return () => {
|
|
2284
|
+
cancelAnimationFrame(animationFrame);
|
|
2285
|
+
observer.disconnect();
|
|
2286
|
+
window.removeEventListener('resize', update);
|
|
2287
|
+
window.removeEventListener('scroll', update, true);
|
|
2288
|
+
};
|
|
2289
|
+
}, [labels, visible]);
|
|
2290
|
+
|
|
2291
|
+
if (!visible) {
|
|
2292
|
+
return null;
|
|
2293
|
+
}
|
|
2294
|
+
|
|
2295
|
+
return (
|
|
2296
|
+
<div aria-hidden="true" className="boundary-overlay">
|
|
2297
|
+
{boxes.map((box, index) => (
|
|
2298
|
+
<div
|
|
2299
|
+
className="boundary-overlay__box"
|
|
2300
|
+
data-boundary-id={box.id}
|
|
2301
|
+
data-label-placement={box.labelPlacement}
|
|
2302
|
+
key={\`\${box.id}-\${index}\`}
|
|
2303
|
+
style={{
|
|
2304
|
+
'--boundary-color': box.color,
|
|
2305
|
+
height: box.height,
|
|
2306
|
+
left: box.left,
|
|
2307
|
+
top: box.top,
|
|
2308
|
+
width: box.width,
|
|
2309
|
+
} as CSSProperties}
|
|
2310
|
+
>
|
|
2311
|
+
<span className="boundary-overlay__label">{box.label}</span>
|
|
2312
|
+
</div>
|
|
2313
|
+
))}
|
|
2314
|
+
</div>
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
|
|
2318
|
+
export default function ${toPascalCase(app.id)}Home() {
|
|
2319
|
+
const { i18nInstance, language } = useModernI18n();
|
|
2320
|
+
const t = i18nInstance.t.bind(i18nInstance);
|
|
2321
|
+
const [cart, setCart] = useState<CartState>({});
|
|
2322
|
+
const [showBoundaries, setShowBoundaries] = useState(false);
|
|
2323
|
+
const [effectApiStatus, setEffectApiStatus] = useState('pending');
|
|
2324
|
+
const boundaryLabels = {
|
|
2325
|
+
checkout: t('commerce.boundaries.checkout'),
|
|
2326
|
+
decide: t('commerce.boundaries.decide'),
|
|
2327
|
+
explore: t('commerce.boundaries.explore'),
|
|
2328
|
+
} satisfies BoundaryLabels;
|
|
2329
|
+
const cartLines = products
|
|
2330
|
+
.map(product => ({
|
|
2331
|
+
product,
|
|
2332
|
+
quantity: cart[product.id] ?? 0,
|
|
2333
|
+
}))
|
|
2334
|
+
.filter(line => line.quantity > 0);
|
|
2335
|
+
const cartCount = cartLines.reduce((total, line) => total + line.quantity, 0);
|
|
2336
|
+
|
|
2337
|
+
useEffect(() => {
|
|
2338
|
+
void fetch('${effectApiPrefix(app)}/effect/${effectApiStem(app)}?limit=1', {
|
|
2339
|
+
headers: {
|
|
2340
|
+
accept: 'application/json',
|
|
2341
|
+
},
|
|
2342
|
+
})
|
|
2343
|
+
.then(response => {
|
|
2344
|
+
if (!response.ok) {
|
|
2345
|
+
throw new Error(\`Effect BFF request failed: \${response.status}\`);
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
return response.json() as Promise<{ items?: Array<{ title?: string }> }>;
|
|
2349
|
+
})
|
|
2350
|
+
.then(data => {
|
|
2351
|
+
setEffectApiStatus(data.items[0]?.title ?? 'empty');
|
|
2352
|
+
})
|
|
2353
|
+
.catch(() => {
|
|
2354
|
+
setEffectApiStatus('unavailable');
|
|
2355
|
+
});
|
|
2356
|
+
}, []);
|
|
2357
|
+
|
|
2358
|
+
const addToCart = (id: ProductId) => {
|
|
2359
|
+
setCart(current => ({
|
|
2360
|
+
...current,
|
|
2361
|
+
[id]: (current[id] ?? 0) + 1,
|
|
2362
|
+
}));
|
|
2363
|
+
};
|
|
2364
|
+
|
|
2365
|
+
const reduceQuantity = (id: ProductId) => {
|
|
2366
|
+
setCart(current => {
|
|
2367
|
+
const quantity = current[id] ?? 0;
|
|
2368
|
+
const next = { ...current };
|
|
2369
|
+
|
|
2370
|
+
if (quantity <= 1) {
|
|
2371
|
+
delete next[id];
|
|
2372
|
+
} else {
|
|
2373
|
+
next[id] = quantity - 1;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
return next;
|
|
2377
|
+
});
|
|
2378
|
+
};
|
|
2379
|
+
|
|
2380
|
+
const removeFromCart = (id: ProductId) => {
|
|
2381
|
+
setCart(current => {
|
|
2382
|
+
const next = { ...current };
|
|
2383
|
+
|
|
2384
|
+
delete next[id];
|
|
2385
|
+
return next;
|
|
2386
|
+
});
|
|
2387
|
+
};
|
|
2388
|
+
|
|
2389
|
+
return (
|
|
2390
|
+
<main className="commerce-shell">
|
|
2391
|
+
<LocalizedHead />
|
|
2392
|
+
<BoundaryOverlay labels={boundaryLabels} visible={showBoundaries} />
|
|
2393
|
+
<header className="commerce-header" data-boundary="explore">
|
|
2394
|
+
<strong className="commerce-logo">{t('commerce.brand')}</strong>
|
|
2395
|
+
<nav aria-label={t('commerce.navigation.primary')} className="commerce-nav">
|
|
2396
|
+
<a className="commerce-pill" href="#machines">
|
|
2397
|
+
{t('commerce.navigation.machines')}
|
|
2398
|
+
</a>
|
|
2399
|
+
<a className="commerce-pill" href="#checkout">
|
|
2400
|
+
{t('commerce.navigation.checkout')}
|
|
2401
|
+
</a>
|
|
2402
|
+
</nav>
|
|
2403
|
+
<div className="commerce-actions">
|
|
2404
|
+
<a className="commerce-cart-button" data-boundary="checkout" href="#cart">
|
|
2405
|
+
{t('commerce.cart.button', { count: cartCount })}
|
|
2406
|
+
</a>
|
|
2407
|
+
<nav aria-label={t('commerce.language.switcher')} className="commerce-language">
|
|
2408
|
+
{languageCodes.map(code => (
|
|
2409
|
+
<a
|
|
2410
|
+
aria-current={language === code ? 'page' : undefined}
|
|
2411
|
+
className="commerce-pill"
|
|
2412
|
+
href={\`/\${code}\`}
|
|
2413
|
+
key={code}
|
|
2414
|
+
>
|
|
2415
|
+
{t(\`commerce.language.\${code}\`)}
|
|
2416
|
+
</a>
|
|
2417
|
+
))}
|
|
2418
|
+
</nav>
|
|
2419
|
+
</div>
|
|
2420
|
+
</header>
|
|
2421
|
+
|
|
2422
|
+
<div className="commerce-page">
|
|
2423
|
+
<section className="commerce-product" data-boundary-page="decide" id="machines">
|
|
2424
|
+
<div
|
|
2425
|
+
aria-label={t('commerce.products.fieldLoader.imageAlt')}
|
|
2426
|
+
className="commerce-product-media"
|
|
2427
|
+
role="img"
|
|
2428
|
+
/>
|
|
2429
|
+
<div>
|
|
2430
|
+
<p className="commerce-eyebrow">{t('commerce.detail.eyebrow')}</p>
|
|
2431
|
+
<h1 className="commerce-title">{t(featuredProduct.titleKey)}</h1>
|
|
2432
|
+
<p className="commerce-lede">{t(featuredProduct.descriptionKey)}</p>
|
|
2433
|
+
<div className="commerce-facts">
|
|
2434
|
+
<div className="commerce-fact">
|
|
2435
|
+
<span>{t('commerce.detail.price')}</span>
|
|
2436
|
+
<strong>{t(featuredProduct.priceKey)}</strong>
|
|
2437
|
+
</div>
|
|
2438
|
+
<div className="commerce-fact">
|
|
2439
|
+
<span>{t('commerce.detail.power')}</span>
|
|
2440
|
+
<strong>{t(featuredProduct.powerKey)}</strong>
|
|
2441
|
+
</div>
|
|
2442
|
+
<div className="commerce-fact">
|
|
2443
|
+
<span>{t('commerce.detail.availability')}</span>
|
|
2444
|
+
<strong>{t(featuredProduct.availabilityKey)}</strong>
|
|
2445
|
+
</div>
|
|
2446
|
+
</div>
|
|
2447
|
+
<div className="commerce-checkout" data-boundary="checkout" id="checkout">
|
|
2448
|
+
<button
|
|
2449
|
+
className="commerce-button"
|
|
2450
|
+
onClick={() => addToCart(featuredProduct.id)}
|
|
2451
|
+
type="button"
|
|
2452
|
+
>
|
|
2453
|
+
{t('commerce.cart.add')}
|
|
2454
|
+
</button>
|
|
2455
|
+
<a className="commerce-link-button" href="#cart">
|
|
2456
|
+
{t('commerce.cart.view')}
|
|
2457
|
+
</a>
|
|
2458
|
+
</div>
|
|
2459
|
+
</div>
|
|
2460
|
+
</section>
|
|
2461
|
+
|
|
2462
|
+
<section data-boundary="explore">
|
|
2463
|
+
<h2 className="commerce-section-title">{t('commerce.recommendations.title')}</h2>
|
|
2464
|
+
<div className="commerce-grid">
|
|
2465
|
+
{recommendations.map(product => (
|
|
2466
|
+
<article className="commerce-card" key={product.id}>
|
|
2467
|
+
<span>{t(product.badgeKey)}</span>
|
|
2468
|
+
<strong>{t(product.titleKey)}</strong>
|
|
2469
|
+
</article>
|
|
2470
|
+
))}
|
|
2471
|
+
</div>
|
|
2472
|
+
</section>
|
|
2473
|
+
|
|
2474
|
+
<section className="commerce-cart-panel" data-boundary="checkout" id="cart">
|
|
2475
|
+
<h2>{t('commerce.cart.title')}</h2>
|
|
2476
|
+
{cartLines.length === 0 ? (
|
|
2477
|
+
<p>{t('commerce.cart.empty')}</p>
|
|
2478
|
+
) : (
|
|
2479
|
+
cartLines.map(line => (
|
|
2480
|
+
<div className="commerce-cart-line" key={line.product.id}>
|
|
2481
|
+
<strong>{t(line.product.titleKey)}</strong>
|
|
2482
|
+
<div className="commerce-quantity">
|
|
2483
|
+
<button
|
|
2484
|
+
aria-label={t('commerce.cart.decrease', {
|
|
2485
|
+
name: t(line.product.titleKey),
|
|
2486
|
+
})}
|
|
2487
|
+
className="commerce-quantity-button"
|
|
2488
|
+
onClick={() => reduceQuantity(line.product.id)}
|
|
2489
|
+
type="button"
|
|
2490
|
+
>
|
|
2491
|
+
-
|
|
2492
|
+
</button>
|
|
2493
|
+
<span>{line.quantity}</span>
|
|
2494
|
+
<button
|
|
2495
|
+
aria-label={t('commerce.cart.increase', {
|
|
2496
|
+
name: t(line.product.titleKey),
|
|
2497
|
+
})}
|
|
2498
|
+
className="commerce-quantity-button"
|
|
2499
|
+
onClick={() => addToCart(line.product.id)}
|
|
2500
|
+
type="button"
|
|
2501
|
+
>
|
|
2502
|
+
+
|
|
2503
|
+
</button>
|
|
2504
|
+
<button
|
|
2505
|
+
className="commerce-link-button"
|
|
2506
|
+
onClick={() => removeFromCart(line.product.id)}
|
|
2507
|
+
type="button"
|
|
2508
|
+
>
|
|
2509
|
+
{t('commerce.cart.remove')}
|
|
2510
|
+
</button>
|
|
2511
|
+
</div>
|
|
2512
|
+
</div>
|
|
2513
|
+
))
|
|
2514
|
+
)}
|
|
2515
|
+
</section>
|
|
2516
|
+
</div>
|
|
2517
|
+
|
|
2518
|
+
<footer className="commerce-footer" data-boundary="explore">
|
|
2519
|
+
<span>{t('commerce.footer.stack')}</span>
|
|
2520
|
+
<span data-testid="effect-bff-status">{effectApiStatus}</span>
|
|
2521
|
+
<span data-build-marker={ultramodernUiMarker.build} data-testid="ultramodern-ui-marker">
|
|
2522
|
+
{ultramodernUiMarker.appId}:{ultramodernUiMarker.version}
|
|
2523
|
+
</span>
|
|
2524
|
+
</footer>
|
|
2525
|
+
|
|
2526
|
+
<label className="commerce-boundary-toggle">
|
|
2527
|
+
<input
|
|
2528
|
+
checked={showBoundaries}
|
|
2529
|
+
onChange={event => setShowBoundaries(event.currentTarget.checked)}
|
|
2530
|
+
type="checkbox"
|
|
2531
|
+
/>
|
|
2532
|
+
{t('commerce.boundaries.toggle')}
|
|
2533
|
+
</label>
|
|
1213
2534
|
</main>
|
|
1214
2535
|
);
|
|
1215
2536
|
}
|
|
1216
2537
|
`;
|
|
1217
2538
|
}
|
|
1218
2539
|
function createLayout(appId) {
|
|
1219
|
-
return `import
|
|
2540
|
+
return `import { Outlet } from '@modern-js/plugin-tanstack/runtime';
|
|
2541
|
+
import './index.css';
|
|
1220
2542
|
|
|
1221
|
-
export default function Layout(
|
|
1222
|
-
return
|
|
2543
|
+
export default function Layout() {
|
|
2544
|
+
return (
|
|
2545
|
+
<div data-app-id="${appId}">
|
|
2546
|
+
<Outlet />
|
|
2547
|
+
</div>
|
|
2548
|
+
);
|
|
1223
2549
|
}
|
|
1224
2550
|
`;
|
|
1225
2551
|
}
|
|
1226
2552
|
function createRemoteEntry(app) {
|
|
1227
|
-
|
|
1228
|
-
return `export { default } from './components/${componentName}';
|
|
2553
|
+
return `export { default } from './components/${remoteWidgetFile(app)}';
|
|
1229
2554
|
`;
|
|
1230
2555
|
}
|
|
1231
2556
|
function createRemoteWidget(app) {
|
|
1232
|
-
const componentName =
|
|
2557
|
+
const componentName = `${toPascalCase(app.domain ?? app.id)}Widget`;
|
|
2558
|
+
const body = 'vertical' === app.kind ? `Owns the ${app.domain} vertical route surface.` : 'Provides shared UI primitives for the workspace.';
|
|
1233
2559
|
return `export default function ${componentName}() {
|
|
1234
2560
|
return (
|
|
1235
2561
|
<section data-mf-remote="${app.id}">
|
|
1236
2562
|
<h2>${app.displayName}</h2>
|
|
1237
|
-
<p
|
|
2563
|
+
<p>${body}</p>
|
|
1238
2564
|
</section>
|
|
1239
2565
|
);
|
|
1240
2566
|
}
|
|
1241
2567
|
`;
|
|
1242
2568
|
}
|
|
2569
|
+
function createAppLocaleMessages(app, language) {
|
|
2570
|
+
const czechLabels = {
|
|
2571
|
+
commerce: {
|
|
2572
|
+
role: 'obchod',
|
|
2573
|
+
title: 'Obchodní remote'
|
|
2574
|
+
},
|
|
2575
|
+
'design-system': {
|
|
2576
|
+
role: 'design system',
|
|
2577
|
+
title: 'Design system remote'
|
|
2578
|
+
},
|
|
2579
|
+
identity: {
|
|
2580
|
+
role: 'identita',
|
|
2581
|
+
title: 'Identitní remote'
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
if ('shell' === app.kind) return {
|
|
2585
|
+
shell: {
|
|
2586
|
+
language: {
|
|
2587
|
+
cs: 'en' === language ? 'Czech' : 'Čeština',
|
|
2588
|
+
en: 'en' === language ? 'English' : 'Angličtina',
|
|
2589
|
+
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2590
|
+
},
|
|
2591
|
+
remotes: {
|
|
2592
|
+
commerce: 'en' === language ? 'Commerce Remote' : 'Obchodní remote',
|
|
2593
|
+
designSystem: 'en' === language ? 'Design System Remote' : 'Design system remote',
|
|
2594
|
+
identity: 'en' === language ? 'Identity Remote' : 'Identitní remote'
|
|
2595
|
+
},
|
|
2596
|
+
title: 'en' === language ? 'UltraModern SuperApp Shell' : 'UltraModern SuperApp shell'
|
|
2597
|
+
}
|
|
2598
|
+
};
|
|
2599
|
+
const domain = app.domain ?? app.id;
|
|
2600
|
+
const czechLabel = czechLabels[domain] ?? {
|
|
2601
|
+
role: domain,
|
|
2602
|
+
title: `${app.displayName} CZ`
|
|
2603
|
+
};
|
|
2604
|
+
if ('commerce' === domain) return {
|
|
2605
|
+
commerce: {
|
|
2606
|
+
boundaries: {
|
|
2607
|
+
checkout: 'en' === language ? 'checkout' : 'pokladna',
|
|
2608
|
+
decide: 'en' === language ? 'decide' : 'rozhodování',
|
|
2609
|
+
explore: 'en' === language ? 'explore' : 'procházení',
|
|
2610
|
+
toggle: 'en' === language ? 'show team boundaries' : 'zobrazit hranice týmů'
|
|
2611
|
+
},
|
|
2612
|
+
brand: 'Acre & Iron',
|
|
2613
|
+
cart: {
|
|
2614
|
+
add: 'en' === language ? 'Add to cart' : 'Přidat do košíku',
|
|
2615
|
+
button: 'en' === language ? 'Your cart ({{count}})' : 'Košík ({{count}})',
|
|
2616
|
+
decrease: 'en' === language ? 'Decrease {{name}} quantity' : 'Snížit množství položky {{name}}',
|
|
2617
|
+
empty: 'en' === language ? 'Your cart is empty.' : 'Košík je prázdný.',
|
|
2618
|
+
increase: 'en' === language ? 'Increase {{name}} quantity' : 'Zvýšit množství položky {{name}}',
|
|
2619
|
+
remove: 'en' === language ? 'Remove' : 'Odebrat',
|
|
2620
|
+
title: 'en' === language ? 'Cart' : 'Košík',
|
|
2621
|
+
view: 'en' === language ? 'View cart' : 'Zobrazit košík'
|
|
2622
|
+
},
|
|
2623
|
+
detail: {
|
|
2624
|
+
availability: 'en' === language ? 'Availability' : 'Dostupnost',
|
|
2625
|
+
eyebrow: 'en' === language ? 'Machine detail' : 'Detail stroje',
|
|
2626
|
+
power: 'en' === language ? 'Power' : 'Výkon',
|
|
2627
|
+
price: 'en' === language ? 'Price' : 'Cena'
|
|
2628
|
+
},
|
|
2629
|
+
footer: {
|
|
2630
|
+
stack: 'en' === language ? 'SPA, SSR-ready Module Federation, React, Effect BFF' : 'SPA, SSR-ready Module Federation, React, Effect BFF'
|
|
2631
|
+
},
|
|
2632
|
+
language: {
|
|
2633
|
+
cs: 'en' === language ? 'Czech' : 'Čeština',
|
|
2634
|
+
en: 'en' === language ? 'English' : 'Angličtina',
|
|
2635
|
+
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2636
|
+
},
|
|
2637
|
+
navigation: {
|
|
2638
|
+
checkout: 'en' === language ? 'Checkout' : 'Pokladna',
|
|
2639
|
+
machines: 'en' === language ? 'Machines' : 'Stroje',
|
|
2640
|
+
primary: 'en' === language ? 'Primary commerce navigation' : 'Hlavní navigace obchodu'
|
|
2641
|
+
},
|
|
2642
|
+
products: {
|
|
2643
|
+
autonomy: {
|
|
2644
|
+
badge: 'en' === language ? 'AI-first option' : 'AI varianta',
|
|
2645
|
+
title: 'en' === language ? 'Autonomy Retrofit Kit' : 'Sada pro autonomní řízení'
|
|
2646
|
+
},
|
|
2647
|
+
fieldLoader: {
|
|
2648
|
+
availability: 'en' === language ? 'In stock' : 'Skladem',
|
|
2649
|
+
description: 'en' === language ? 'A loader-ready tractor for feed, hay, gravel, and winter road work.' : 'Traktor připravený na nakladač pro krmivo, seno, štěrk i zimní údržbu cest.',
|
|
2650
|
+
imageAlt: 'en' === language ? 'Field Loader 112 tractor working on a bright farm lane' : 'Traktor Field Loader 112 pracuje na světlé polní cestě',
|
|
2651
|
+
power: '112 hp',
|
|
2652
|
+
price: 'EUR 42,500',
|
|
2653
|
+
title: 'Field Loader 112'
|
|
2654
|
+
},
|
|
2655
|
+
orchard: {
|
|
2656
|
+
badge: 'en' === language ? 'Best for tight rows' : 'Nejlepší do úzkých řádků',
|
|
2657
|
+
title: 'en' === language ? 'Narrow Orchard Tractor' : 'Úzký sadový traktor'
|
|
2658
|
+
}
|
|
2659
|
+
},
|
|
2660
|
+
recommendations: {
|
|
2661
|
+
title: 'en' === language ? 'Compare alternatives' : 'Porovnat alternativy'
|
|
2662
|
+
},
|
|
2663
|
+
role: 'en' === language ? 'commerce' : 'obchod',
|
|
2664
|
+
title: 'en' === language ? app.displayName : czechLabel.title
|
|
2665
|
+
}
|
|
2666
|
+
};
|
|
2667
|
+
return {
|
|
2668
|
+
[domain]: {
|
|
2669
|
+
language: {
|
|
2670
|
+
cs: 'en' === language ? 'Czech' : 'Čeština',
|
|
2671
|
+
en: 'en' === language ? 'English' : 'Angličtina',
|
|
2672
|
+
switcher: 'en' === language ? 'Language' : 'Jazyk'
|
|
2673
|
+
},
|
|
2674
|
+
role: 'en' === language ? app.domain ?? app.kind : czechLabel.role,
|
|
2675
|
+
title: 'en' === language ? app.displayName : czechLabel.title
|
|
2676
|
+
}
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
1243
2679
|
function createDesignButton() {
|
|
1244
2680
|
return `import { designTokens } from '../tokens';
|
|
1245
2681
|
|
|
1246
|
-
export default function Button({ label
|
|
2682
|
+
export default function Button({ label }: { label: string }) {
|
|
1247
2683
|
return (
|
|
1248
2684
|
<button
|
|
1249
2685
|
type="button"
|
|
@@ -1261,8 +2697,8 @@ export default function Button({ label = 'Design System Button' }: { label?: str
|
|
|
1261
2697
|
function createDesignTokens() {
|
|
1262
2698
|
return `export const designTokens = {
|
|
1263
2699
|
color: {
|
|
1264
|
-
foreground: '#133225',
|
|
1265
2700
|
accent: '#2f8f68',
|
|
2701
|
+
foreground: '#133225',
|
|
1266
2702
|
},
|
|
1267
2703
|
radius: {
|
|
1268
2704
|
control: '999px',
|
|
@@ -1270,70 +2706,373 @@ function createDesignTokens() {
|
|
|
1270
2706
|
} as const;
|
|
1271
2707
|
`;
|
|
1272
2708
|
}
|
|
1273
|
-
function
|
|
2709
|
+
function serviceEffectApiExport(service = effectService) {
|
|
2710
|
+
return `${toCamelCase(effectApiStem(service))}EffectApi`;
|
|
2711
|
+
}
|
|
2712
|
+
function serviceEffectGroupName(service = effectService) {
|
|
2713
|
+
return toCamelCase(effectApiStem(service));
|
|
2714
|
+
}
|
|
2715
|
+
function serviceEffectApiName(service = effectService) {
|
|
2716
|
+
return `${toPascalCase(effectApiStem(service))}EffectApi`;
|
|
2717
|
+
}
|
|
2718
|
+
function serviceEffectSchemaExport(service = effectService) {
|
|
2719
|
+
return `${toCamelCase(effectApiStem(service))}ItemSchema`;
|
|
2720
|
+
}
|
|
2721
|
+
function serviceEffectErrorStem(service = effectService) {
|
|
2722
|
+
const stem = effectApiStem(service);
|
|
2723
|
+
return 'recommendations' === stem ? 'recommendation' : stem;
|
|
2724
|
+
}
|
|
2725
|
+
function serviceEffectCreatePayloadSchemaExport(service = effectService) {
|
|
2726
|
+
return `${toCamelCase(effectApiStem(service))}CreatePayloadSchema`;
|
|
2727
|
+
}
|
|
2728
|
+
function serviceEffectNotFoundErrorExport(service = effectService) {
|
|
2729
|
+
return `${toPascalCase(serviceEffectErrorStem(service))}NotFound`;
|
|
2730
|
+
}
|
|
2731
|
+
function serviceEffectNotFoundSchemaExport(service = effectService) {
|
|
2732
|
+
return `${toCamelCase(serviceEffectErrorStem(service))}NotFoundSchema`;
|
|
2733
|
+
}
|
|
2734
|
+
function createEffectSharedApiImports() {
|
|
1274
2735
|
return `import {
|
|
1275
2736
|
HttpApi,
|
|
1276
2737
|
HttpApiEndpoint,
|
|
1277
2738
|
HttpApiGroup,
|
|
2739
|
+
HttpApiSchema,
|
|
1278
2740
|
Schema,
|
|
1279
2741
|
} from '@modern-js/plugin-bff/effect-client';
|
|
1280
|
-
|
|
1281
|
-
|
|
2742
|
+
`;
|
|
2743
|
+
}
|
|
2744
|
+
function createEffectSharedApiContract(service = effectService) {
|
|
2745
|
+
const schemaExport = serviceEffectSchemaExport(service);
|
|
2746
|
+
const createPayloadSchemaExport = serviceEffectCreatePayloadSchemaExport(service);
|
|
2747
|
+
const notFoundErrorExport = serviceEffectNotFoundErrorExport(service);
|
|
2748
|
+
const notFoundSchemaExport = serviceEffectNotFoundSchemaExport(service);
|
|
2749
|
+
const apiExport = serviceEffectApiExport(service);
|
|
2750
|
+
const apiName = serviceEffectApiName(service);
|
|
2751
|
+
const groupName = serviceEffectGroupName(service);
|
|
2752
|
+
const stem = effectApiStem(service);
|
|
2753
|
+
const servicePrefix = effectApiPrefix(service);
|
|
2754
|
+
return `export const ${schemaExport} = Schema.Struct({
|
|
1282
2755
|
id: Schema.String,
|
|
2756
|
+
marker: Schema.Struct({
|
|
2757
|
+
appId: Schema.String,
|
|
2758
|
+
packageName: Schema.String,
|
|
2759
|
+
version: Schema.String,
|
|
2760
|
+
build: Schema.String,
|
|
2761
|
+
deployProfile: Schema.String,
|
|
2762
|
+
surface: Schema.String,
|
|
2763
|
+
}),
|
|
2764
|
+
title: Schema.String,
|
|
2765
|
+
});
|
|
2766
|
+
|
|
2767
|
+
export const ${createPayloadSchemaExport} = Schema.Struct({
|
|
1283
2768
|
title: Schema.String,
|
|
1284
2769
|
});
|
|
1285
2770
|
|
|
1286
|
-
export
|
|
1287
|
-
'
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
2771
|
+
export class ${notFoundErrorExport} extends Schema.TaggedErrorClass<${notFoundErrorExport}>()(
|
|
2772
|
+
'${notFoundErrorExport}',
|
|
2773
|
+
{
|
|
2774
|
+
id: Schema.String,
|
|
2775
|
+
},
|
|
2776
|
+
) {}
|
|
2777
|
+
|
|
2778
|
+
export const ${notFoundSchemaExport} = ${notFoundErrorExport}.pipe(
|
|
2779
|
+
HttpApiSchema.status(404),
|
|
2780
|
+
);
|
|
2781
|
+
|
|
2782
|
+
export type OperationContext = {
|
|
2783
|
+
operationId: string;
|
|
2784
|
+
routePath: string;
|
|
2785
|
+
method: string;
|
|
2786
|
+
source: string;
|
|
2787
|
+
traceId?: string;
|
|
2788
|
+
};
|
|
2789
|
+
|
|
2790
|
+
export const ${apiExport} = HttpApi.make('${apiName}').add(
|
|
2791
|
+
HttpApiGroup.make('${groupName}')
|
|
2792
|
+
.add(
|
|
2793
|
+
HttpApiEndpoint.get('list', '/effect/${stem}', {
|
|
2794
|
+
query: {
|
|
2795
|
+
limit: Schema.optional(Schema.NumberFromString),
|
|
2796
|
+
},
|
|
2797
|
+
success: Schema.Struct({
|
|
2798
|
+
items: Schema.Array(${schemaExport}),
|
|
2799
|
+
}),
|
|
1293
2800
|
}),
|
|
1294
|
-
|
|
1295
|
-
|
|
2801
|
+
)
|
|
2802
|
+
.add(
|
|
2803
|
+
HttpApiEndpoint.get('get', '/effect/${stem}/:id', {
|
|
2804
|
+
params: {
|
|
2805
|
+
id: Schema.String,
|
|
2806
|
+
},
|
|
2807
|
+
success: ${schemaExport},
|
|
2808
|
+
error: ${notFoundSchemaExport},
|
|
2809
|
+
}),
|
|
2810
|
+
)
|
|
2811
|
+
.add(
|
|
2812
|
+
HttpApiEndpoint.post('create', '/effect/${stem}', {
|
|
2813
|
+
payload: ${createPayloadSchemaExport},
|
|
2814
|
+
success: Schema.Struct({
|
|
2815
|
+
item: ${schemaExport},
|
|
2816
|
+
}),
|
|
2817
|
+
}),
|
|
2818
|
+
),
|
|
1296
2819
|
);
|
|
2820
|
+
|
|
2821
|
+
export const ${groupName}OperationContexts = {
|
|
2822
|
+
list: {
|
|
2823
|
+
operationId: '${apiName}:${groupName}:list',
|
|
2824
|
+
routePath: '/effect/${stem}',
|
|
2825
|
+
method: 'GET',
|
|
2826
|
+
source: 'generated-client',
|
|
2827
|
+
},
|
|
2828
|
+
get: {
|
|
2829
|
+
operationId: '${apiName}:${groupName}:get',
|
|
2830
|
+
routePath: '/effect/${stem}/:id',
|
|
2831
|
+
method: 'GET',
|
|
2832
|
+
source: 'generated-client',
|
|
2833
|
+
},
|
|
2834
|
+
create: {
|
|
2835
|
+
operationId: '${apiName}:${groupName}:create',
|
|
2836
|
+
routePath: '/effect/${stem}',
|
|
2837
|
+
method: 'POST',
|
|
2838
|
+
source: 'generated-client',
|
|
2839
|
+
},
|
|
2840
|
+
} satisfies Record<string, OperationContext>;
|
|
2841
|
+
|
|
2842
|
+
export const ${groupName}ApiContract = {
|
|
2843
|
+
basePath: '${servicePrefix}/effect/${stem}',
|
|
2844
|
+
ownerId: '${service.id}',
|
|
2845
|
+
servicePrefix: '${servicePrefix}',
|
|
2846
|
+
} as const;
|
|
2847
|
+
`;
|
|
2848
|
+
}
|
|
2849
|
+
function createEffectSharedApi(service) {
|
|
2850
|
+
if (service) return `${createEffectSharedApiImports()}
|
|
2851
|
+
${createEffectSharedApiContract(service)}`;
|
|
2852
|
+
return `export const sharedEffectApiPackage = {
|
|
2853
|
+
scope: 'external-effect-service-contracts',
|
|
2854
|
+
} as const;
|
|
1297
2855
|
`;
|
|
1298
2856
|
}
|
|
1299
|
-
function createEffectServiceEntry() {
|
|
2857
|
+
function createEffectServiceEntry(scope, service = effectService, contractImportPath = ultramodern_workspace_packageName(scope, 'shared-effect-api')) {
|
|
2858
|
+
const apiExport = serviceEffectApiExport(service);
|
|
2859
|
+
const groupName = serviceEffectGroupName(service);
|
|
2860
|
+
const notFoundErrorExport = serviceEffectNotFoundErrorExport(service);
|
|
2861
|
+
const stem = effectApiStem(service);
|
|
1300
2862
|
return `import {
|
|
1301
2863
|
defineEffectBff,
|
|
1302
2864
|
Effect,
|
|
1303
2865
|
HttpApiBuilder,
|
|
1304
2866
|
Layer,
|
|
1305
|
-
} from '@modern-js/plugin-bff/effect-
|
|
1306
|
-
import {
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
2867
|
+
} from '@modern-js/plugin-bff/effect-edge';
|
|
2868
|
+
import { ultramodernApiMarker } from '../../src/ultramodern-build';
|
|
2869
|
+
import {
|
|
2870
|
+
${apiExport},
|
|
2871
|
+
${groupName}OperationContexts,
|
|
2872
|
+
${notFoundErrorExport},
|
|
2873
|
+
type OperationContext,
|
|
2874
|
+
} from '${contractImportPath}';
|
|
2875
|
+
|
|
2876
|
+
const ${groupName}Items = [
|
|
2877
|
+
{
|
|
2878
|
+
id: 'starter-${stem}',
|
|
2879
|
+
marker: ultramodernApiMarker,
|
|
2880
|
+
title: 'Wire a real ${stem} source here',
|
|
2881
|
+
},
|
|
2882
|
+
];
|
|
2883
|
+
|
|
2884
|
+
const operationAttributes = (operationContext: OperationContext) => {
|
|
2885
|
+
return {
|
|
2886
|
+
'modernjs.operation.id': operationContext.operationId,
|
|
2887
|
+
'modernjs.operation.method': operationContext.method,
|
|
2888
|
+
'modernjs.operation.route': operationContext.routePath,
|
|
2889
|
+
'modernjs.operation.source': operationContext.source,
|
|
2890
|
+
...(typeof operationContext.traceId === 'string'
|
|
2891
|
+
? { 'modernjs.trace.id': operationContext.traceId }
|
|
2892
|
+
: {}),
|
|
2893
|
+
};
|
|
2894
|
+
};
|
|
2895
|
+
|
|
2896
|
+
const ${groupName}Layer = HttpApiBuilder.group(
|
|
2897
|
+
${apiExport},
|
|
2898
|
+
'${groupName}',
|
|
2899
|
+
(handlers) =>
|
|
2900
|
+
handlers
|
|
2901
|
+
.handle('list', ({ query }) =>
|
|
2902
|
+
Effect.succeed({
|
|
2903
|
+
items:
|
|
2904
|
+
typeof query.limit === 'number'
|
|
2905
|
+
? ${groupName}Items.slice(0, query.limit)
|
|
2906
|
+
: ${groupName}Items,
|
|
2907
|
+
}).pipe(
|
|
2908
|
+
Effect.withSpan('ultramodern.effect.${groupName}.list', {
|
|
2909
|
+
attributes: operationAttributes(${groupName}OperationContexts.list),
|
|
2910
|
+
kind: 'server',
|
|
2911
|
+
}),
|
|
2912
|
+
),
|
|
2913
|
+
)
|
|
2914
|
+
.handle('get', ({ params }) => {
|
|
2915
|
+
const item = ${groupName}Items.find(item => item.id === params.id);
|
|
2916
|
+
return (item !== undefined
|
|
2917
|
+
? Effect.succeed(item)
|
|
2918
|
+
: Effect.fail(new ${notFoundErrorExport}({ id: params.id }))).pipe(
|
|
2919
|
+
Effect.withSpan('ultramodern.effect.${groupName}.get', {
|
|
2920
|
+
attributes: operationAttributes(${groupName}OperationContexts.get),
|
|
2921
|
+
kind: 'server',
|
|
2922
|
+
}),
|
|
2923
|
+
);
|
|
2924
|
+
})
|
|
2925
|
+
.handle('create', ({ payload }) =>
|
|
2926
|
+
Effect.succeed({
|
|
2927
|
+
item: {
|
|
2928
|
+
id: \`generated-${stem}-\${payload.title
|
|
2929
|
+
.toLowerCase()
|
|
2930
|
+
.replaceAll(/[^a-z0-9]+/g, '-')
|
|
2931
|
+
.replaceAll(/^-|-$/g, '')}\`,
|
|
2932
|
+
marker: ultramodernApiMarker,
|
|
2933
|
+
title: payload.title,
|
|
1318
2934
|
},
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
2935
|
+
}).pipe(
|
|
2936
|
+
Effect.withSpan('ultramodern.effect.${groupName}.create', {
|
|
2937
|
+
attributes: operationAttributes(${groupName}OperationContexts.create),
|
|
2938
|
+
kind: 'server',
|
|
2939
|
+
}),
|
|
2940
|
+
),
|
|
2941
|
+
),
|
|
1322
2942
|
);
|
|
1323
2943
|
|
|
1324
|
-
const layer = HttpApiBuilder.layer(
|
|
1325
|
-
Layer.provide(
|
|
2944
|
+
const layer = HttpApiBuilder.layer(${apiExport}).pipe(
|
|
2945
|
+
Layer.provide(${groupName}Layer),
|
|
1326
2946
|
);
|
|
1327
2947
|
|
|
1328
2948
|
export default defineEffectBff({
|
|
1329
|
-
api:
|
|
2949
|
+
api: ${apiExport},
|
|
1330
2950
|
layer,
|
|
1331
2951
|
});
|
|
1332
2952
|
`;
|
|
1333
2953
|
}
|
|
2954
|
+
function createEffectClient(service, contractImportPath) {
|
|
2955
|
+
const apiExport = serviceEffectApiExport(service);
|
|
2956
|
+
const contractExport = serviceEffectGroupName(service);
|
|
2957
|
+
const stem = effectApiStem(service);
|
|
2958
|
+
const groupName = serviceEffectGroupName(service);
|
|
2959
|
+
const singular = serviceEffectErrorStem(service);
|
|
2960
|
+
const clientOptionsName = `${toPascalCase(stem)}ClientOptions`;
|
|
2961
|
+
const createClientName = `create${toPascalCase(stem)}Client`;
|
|
2962
|
+
const listName = `list${toPascalCase(stem)}`;
|
|
2963
|
+
const getName = `get${toPascalCase(singular)}`;
|
|
2964
|
+
const createName = `create${toPascalCase(singular)}`;
|
|
2965
|
+
return `import {
|
|
2966
|
+
makeEffectHttpApiClient,
|
|
2967
|
+
runEffectRequest,
|
|
2968
|
+
} from '@modern-js/plugin-bff/effect-client';
|
|
2969
|
+
import {
|
|
2970
|
+
${contractExport}ApiContract,
|
|
2971
|
+
${apiExport},
|
|
2972
|
+
${groupName}OperationContexts,
|
|
2973
|
+
type OperationContext,
|
|
2974
|
+
} from '${contractImportPath}';
|
|
2975
|
+
|
|
2976
|
+
export type ${clientOptionsName} = {
|
|
2977
|
+
baseUrl?: string | URL;
|
|
2978
|
+
locale?: string;
|
|
2979
|
+
operationContext?: OperationContext;
|
|
2980
|
+
traceparent?: string;
|
|
2981
|
+
};
|
|
2982
|
+
|
|
2983
|
+
export function ${createClientName}(
|
|
2984
|
+
options: ${clientOptionsName} = {},
|
|
2985
|
+
) {
|
|
2986
|
+
return makeEffectHttpApiClient(${apiExport}, {
|
|
2987
|
+
baseUrl: options.baseUrl ?? ${contractExport}ApiContract.servicePrefix,
|
|
2988
|
+
});
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
export function ${listName}(
|
|
2992
|
+
options: ${clientOptionsName} & { limit?: number } = {},
|
|
2993
|
+
) {
|
|
2994
|
+
return runEffectRequest(
|
|
2995
|
+
${createClientName}({
|
|
2996
|
+
...options,
|
|
2997
|
+
operationContext:
|
|
2998
|
+
options.operationContext ?? ${groupName}OperationContexts.list,
|
|
2999
|
+
}),
|
|
3000
|
+
).then(client =>
|
|
3001
|
+
runEffectRequest(
|
|
3002
|
+
client.${groupName}.list({ query: { limit: options.limit } }),
|
|
3003
|
+
),
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
export function ${getName}(
|
|
3008
|
+
id: string,
|
|
3009
|
+
options: ${clientOptionsName} = {},
|
|
3010
|
+
) {
|
|
3011
|
+
return runEffectRequest(
|
|
3012
|
+
${createClientName}({
|
|
3013
|
+
...options,
|
|
3014
|
+
operationContext:
|
|
3015
|
+
options.operationContext ?? ${groupName}OperationContexts.get,
|
|
3016
|
+
}),
|
|
3017
|
+
).then(client =>
|
|
3018
|
+
runEffectRequest(client.${groupName}.get({ params: { id } })),
|
|
3019
|
+
);
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
export function ${createName}(
|
|
3023
|
+
title: string,
|
|
3024
|
+
options: ${clientOptionsName} = {},
|
|
3025
|
+
) {
|
|
3026
|
+
return runEffectRequest(
|
|
3027
|
+
${createClientName}({
|
|
3028
|
+
...options,
|
|
3029
|
+
operationContext:
|
|
3030
|
+
options.operationContext ?? ${groupName}OperationContexts.create,
|
|
3031
|
+
}),
|
|
3032
|
+
).then(client =>
|
|
3033
|
+
runEffectRequest(
|
|
3034
|
+
client.${groupName}.create({ payload: { title } }),
|
|
3035
|
+
),
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
`;
|
|
3039
|
+
}
|
|
3040
|
+
function createShellEffectClient(scope) {
|
|
3041
|
+
return `export {
|
|
3042
|
+
createRecommendation,
|
|
3043
|
+
createRecommendationsClient,
|
|
3044
|
+
getRecommendation,
|
|
3045
|
+
listRecommendations,
|
|
3046
|
+
type RecommendationsClientOptions,
|
|
3047
|
+
} from '${ultramodern_workspace_packageName(scope, 'remote-commerce')}/effect/client';
|
|
3048
|
+
`;
|
|
3049
|
+
}
|
|
1334
3050
|
function toPascalCase(value) {
|
|
1335
3051
|
return value.split(/[-_]+/).filter(Boolean).map((part)=>`${part.charAt(0).toUpperCase()}${part.slice(1)}`).join('');
|
|
1336
3052
|
}
|
|
3053
|
+
function effectApiTopologyMetadata(app) {
|
|
3054
|
+
if (!appHasEffectApi(app)) return;
|
|
3055
|
+
return {
|
|
3056
|
+
effect: {
|
|
3057
|
+
runtime: 'effect',
|
|
3058
|
+
bff: {
|
|
3059
|
+
prefix: app.effectApi.prefix,
|
|
3060
|
+
openapi: '/openapi.json'
|
|
3061
|
+
},
|
|
3062
|
+
contract: {
|
|
3063
|
+
export: './shared/effect/api',
|
|
3064
|
+
path: `${app.directory}/shared/effect/api.ts`
|
|
3065
|
+
},
|
|
3066
|
+
client: {
|
|
3067
|
+
export: './effect/client',
|
|
3068
|
+
path: `${app.directory}/src/effect/${app.effectApi.stem}-client.ts`
|
|
3069
|
+
},
|
|
3070
|
+
serverEntry: `${app.directory}/api/effect/index.ts`,
|
|
3071
|
+
basePath: `${app.effectApi.prefix}/effect/${app.effectApi.stem}`,
|
|
3072
|
+
consumedBy: app.effectApi.consumedBy
|
|
3073
|
+
}
|
|
3074
|
+
};
|
|
3075
|
+
}
|
|
1337
3076
|
function createTopology(scope) {
|
|
1338
3077
|
return {
|
|
1339
3078
|
schemaVersion: 1,
|
|
@@ -1373,25 +3112,12 @@ function createTopology(scope) {
|
|
|
1373
3112
|
fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
|
|
1374
3113
|
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
1375
3114
|
},
|
|
3115
|
+
...effectApiTopologyMetadata(remote) ? {
|
|
3116
|
+
api: effectApiTopologyMetadata(remote)
|
|
3117
|
+
} : {},
|
|
1376
3118
|
ownership: remote.ownership
|
|
1377
3119
|
})),
|
|
1378
|
-
effectServices: [
|
|
1379
|
-
{
|
|
1380
|
-
id: effectService.id,
|
|
1381
|
-
kind: 'effect-service',
|
|
1382
|
-
runtime: 'effect',
|
|
1383
|
-
package: ultramodern_workspace_packageName(scope, effectService.packageSuffix),
|
|
1384
|
-
consumedBy: [
|
|
1385
|
-
shellApp.id,
|
|
1386
|
-
'remote-commerce'
|
|
1387
|
-
],
|
|
1388
|
-
bff: {
|
|
1389
|
-
prefix: '/recommendations-api',
|
|
1390
|
-
openapi: '/openapi.json'
|
|
1391
|
-
},
|
|
1392
|
-
ownership: effectService.ownership
|
|
1393
|
-
}
|
|
1394
|
-
],
|
|
3120
|
+
effectServices: [],
|
|
1395
3121
|
sharedPackages: sharedPackages.map((sharedPackage)=>({
|
|
1396
3122
|
id: sharedPackage.id,
|
|
1397
3123
|
package: ultramodern_workspace_packageName(scope, sharedPackage.id),
|
|
@@ -1401,7 +3127,7 @@ function createTopology(scope) {
|
|
|
1401
3127
|
validation: {
|
|
1402
3128
|
script: "scripts/validate-ultramodern-workspace.mjs",
|
|
1403
3129
|
commands: [
|
|
1404
|
-
'pnpm ultramodern:check'
|
|
3130
|
+
'mise exec -- pnpm ultramodern:check'
|
|
1405
3131
|
]
|
|
1406
3132
|
}
|
|
1407
3133
|
};
|
|
@@ -1413,12 +3139,6 @@ function createOwnership(scope) {
|
|
|
1413
3139
|
owners: [
|
|
1414
3140
|
shellApp,
|
|
1415
3141
|
...remoteApps,
|
|
1416
|
-
{
|
|
1417
|
-
id: effectService.id,
|
|
1418
|
-
packageSuffix: effectService.packageSuffix,
|
|
1419
|
-
directory: effectService.directory,
|
|
1420
|
-
ownership: effectService.ownership
|
|
1421
|
-
},
|
|
1422
3142
|
...sharedPackages.map((sharedPackage)=>({
|
|
1423
3143
|
id: sharedPackage.id,
|
|
1424
3144
|
packageSuffix: sharedPackage.id,
|
|
@@ -1456,19 +3176,15 @@ function createDevelopmentOverlay() {
|
|
|
1456
3176
|
].map((app)=>[
|
|
1457
3177
|
app.id,
|
|
1458
3178
|
app.port
|
|
1459
|
-
])
|
|
1460
|
-
[
|
|
1461
|
-
effectService.id,
|
|
1462
|
-
effectService.port
|
|
1463
|
-
]
|
|
1464
|
-
])),
|
|
3179
|
+
])),
|
|
1465
3180
|
manifests: Object.fromEntries(remoteApps.map((remote)=>[
|
|
1466
3181
|
remote.id,
|
|
1467
3182
|
`http://localhost:${remote.port}/mf-manifest.json`
|
|
1468
3183
|
])),
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
3184
|
+
apis: Object.fromEntries(verticalEffectApps().map((app)=>[
|
|
3185
|
+
app.id,
|
|
3186
|
+
`http://localhost:${app.port}${effectApiPrefix(app)}`
|
|
3187
|
+
]))
|
|
1472
3188
|
};
|
|
1473
3189
|
}
|
|
1474
3190
|
function createPackageSourceMetadata(scope, packageSource) {
|
|
@@ -1498,6 +3214,232 @@ function createPackageSourceMetadata(scope, packageSource) {
|
|
|
1498
3214
|
}
|
|
1499
3215
|
};
|
|
1500
3216
|
}
|
|
3217
|
+
function createEffectOperationContract(target) {
|
|
3218
|
+
const stem = effectApiStem(target);
|
|
3219
|
+
return {
|
|
3220
|
+
group: serviceEffectGroupName(target),
|
|
3221
|
+
notFound: serviceEffectNotFoundErrorExport(target),
|
|
3222
|
+
operations: {
|
|
3223
|
+
list: {
|
|
3224
|
+
method: 'GET',
|
|
3225
|
+
path: `/effect/${stem}`,
|
|
3226
|
+
source: 'generated-client'
|
|
3227
|
+
},
|
|
3228
|
+
get: {
|
|
3229
|
+
method: 'GET',
|
|
3230
|
+
path: `/effect/${stem}/:id`,
|
|
3231
|
+
source: 'generated-client'
|
|
3232
|
+
},
|
|
3233
|
+
create: {
|
|
3234
|
+
method: 'POST',
|
|
3235
|
+
path: `/effect/${stem}`,
|
|
3236
|
+
source: 'generated-client'
|
|
3237
|
+
}
|
|
3238
|
+
}
|
|
3239
|
+
};
|
|
3240
|
+
}
|
|
3241
|
+
function createAppConfigContract(app) {
|
|
3242
|
+
return {
|
|
3243
|
+
preset: 'presetUltramodern',
|
|
3244
|
+
plugins: [
|
|
3245
|
+
'appTools',
|
|
3246
|
+
'tanstackRouterPlugin',
|
|
3247
|
+
'i18nPlugin',
|
|
3248
|
+
...appHasEffectApi(app) ? [
|
|
3249
|
+
'bffPlugin'
|
|
3250
|
+
] : [],
|
|
3251
|
+
'moduleFederationPlugin',
|
|
3252
|
+
'zephyrRspackPlugin'
|
|
3253
|
+
],
|
|
3254
|
+
output: {
|
|
3255
|
+
disableTsChecker: true,
|
|
3256
|
+
distPath: {
|
|
3257
|
+
html: './'
|
|
3258
|
+
},
|
|
3259
|
+
polyfill: 'off',
|
|
3260
|
+
splitRouteChunks: false
|
|
3261
|
+
},
|
|
3262
|
+
performance: {
|
|
3263
|
+
rsdoctor: {
|
|
3264
|
+
enabledByEnv: 'ULTRAMODERN_RSDOCTOR=true',
|
|
3265
|
+
disableClientServer: true
|
|
3266
|
+
}
|
|
3267
|
+
},
|
|
3268
|
+
html: {
|
|
3269
|
+
outputStructure: 'flat'
|
|
3270
|
+
},
|
|
3271
|
+
source: {
|
|
3272
|
+
mainEntryName: 'index',
|
|
3273
|
+
siteUrlGlobal: 'ULTRAMODERN_SITE_URL'
|
|
3274
|
+
},
|
|
3275
|
+
...appHasEffectApi(app) ? {
|
|
3276
|
+
bff: {
|
|
3277
|
+
runtimeFramework: 'effect',
|
|
3278
|
+
prefix: app.effectApi.prefix,
|
|
3279
|
+
openapi: '/openapi.json'
|
|
3280
|
+
}
|
|
3281
|
+
} : {}
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
function createStylingContract(enableTailwind) {
|
|
3285
|
+
return {
|
|
3286
|
+
tailwind: enableTailwind,
|
|
3287
|
+
...enableTailwind ? {
|
|
3288
|
+
postcssPlugins: [
|
|
3289
|
+
'@tailwindcss/postcss'
|
|
3290
|
+
],
|
|
3291
|
+
contentGlobs: [
|
|
3292
|
+
'./src/**/*.{js,jsx,ts,tsx}'
|
|
3293
|
+
]
|
|
3294
|
+
} : {}
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function createAppGeneratedContract(scope, app, apps, enableTailwind) {
|
|
3298
|
+
const remoteAppsForShell = apps.filter((candidate)=>'shell' !== candidate.kind && candidate.mfName);
|
|
3299
|
+
return {
|
|
3300
|
+
id: app.id,
|
|
3301
|
+
package: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
3302
|
+
path: app.directory,
|
|
3303
|
+
kind: app.kind,
|
|
3304
|
+
config: createAppConfigContract(app),
|
|
3305
|
+
styling: createStylingContract(enableTailwind),
|
|
3306
|
+
deploy: {
|
|
3307
|
+
target: 'cloudflare',
|
|
3308
|
+
worker: {
|
|
3309
|
+
ssr: true
|
|
3310
|
+
},
|
|
3311
|
+
output: {
|
|
3312
|
+
flat: true,
|
|
3313
|
+
htmlDistPath: './'
|
|
3314
|
+
}
|
|
3315
|
+
},
|
|
3316
|
+
ssr: {
|
|
3317
|
+
mode: 'stream',
|
|
3318
|
+
moduleFederationAppSSR: true
|
|
3319
|
+
},
|
|
3320
|
+
i18n: {
|
|
3321
|
+
plugin: '@modern-js/plugin-i18n',
|
|
3322
|
+
backend: true,
|
|
3323
|
+
reactI18next: false,
|
|
3324
|
+
languages: [
|
|
3325
|
+
'en',
|
|
3326
|
+
'cs'
|
|
3327
|
+
],
|
|
3328
|
+
fallbackLanguage: 'en',
|
|
3329
|
+
publicDir: './locales'
|
|
3330
|
+
},
|
|
3331
|
+
moduleFederation: {
|
|
3332
|
+
name: app.mfName,
|
|
3333
|
+
...'shell' === app.kind ? {
|
|
3334
|
+
remotes: remoteAppsForShell.map((remote)=>({
|
|
3335
|
+
id: remote.id,
|
|
3336
|
+
alias: remoteDependencyAlias(remote),
|
|
3337
|
+
name: remote.mfName,
|
|
3338
|
+
manifestEnv: createRemoteManifestEnv(remote),
|
|
3339
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
3340
|
+
}))
|
|
3341
|
+
} : {},
|
|
3342
|
+
exposes: Object.keys(app.exposes ?? {}),
|
|
3343
|
+
dts: {
|
|
3344
|
+
displayErrorInTerminal: true,
|
|
3345
|
+
compilerInstance: "--package typescript -- tsc"
|
|
3346
|
+
},
|
|
3347
|
+
browserSafeExposesOnly: true,
|
|
3348
|
+
zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION
|
|
3349
|
+
},
|
|
3350
|
+
marker: {
|
|
3351
|
+
appId: app.id,
|
|
3352
|
+
packageName: ultramodern_workspace_packageName(scope, app.packageSuffix),
|
|
3353
|
+
version: '0.1.0',
|
|
3354
|
+
build: createBuildMarker(scope, app),
|
|
3355
|
+
deployProfile: 'cloudflare-ssr-mf-effect-v1',
|
|
3356
|
+
uiSurface: 'ui',
|
|
3357
|
+
...appHasEffectApi(app) ? {
|
|
3358
|
+
apiSurface: 'effect-bff'
|
|
3359
|
+
} : {}
|
|
3360
|
+
},
|
|
3361
|
+
...'commerce' === app.domain ? {
|
|
3362
|
+
boundaryVisualization: {
|
|
3363
|
+
mode: 'overlay',
|
|
3364
|
+
layoutAffecting: false,
|
|
3365
|
+
toggle: 'user-controlled',
|
|
3366
|
+
boundaries: [
|
|
3367
|
+
{
|
|
3368
|
+
id: 'explore',
|
|
3369
|
+
labelKey: 'commerce.boundaries.explore',
|
|
3370
|
+
owner: 'team-explore',
|
|
3371
|
+
color: '#ff5a57',
|
|
3372
|
+
owns: [
|
|
3373
|
+
'header',
|
|
3374
|
+
'footer',
|
|
3375
|
+
'recommendations',
|
|
3376
|
+
'catalog'
|
|
3377
|
+
]
|
|
3378
|
+
},
|
|
3379
|
+
{
|
|
3380
|
+
id: 'decide',
|
|
3381
|
+
labelKey: 'commerce.boundaries.decide',
|
|
3382
|
+
owner: 'team-decide',
|
|
3383
|
+
color: '#24d671',
|
|
3384
|
+
owns: [
|
|
3385
|
+
'product-detail',
|
|
3386
|
+
'variant-selection'
|
|
3387
|
+
]
|
|
3388
|
+
},
|
|
3389
|
+
{
|
|
3390
|
+
id: 'checkout',
|
|
3391
|
+
labelKey: 'commerce.boundaries.checkout',
|
|
3392
|
+
owner: 'team-checkout',
|
|
3393
|
+
color: '#f4d044',
|
|
3394
|
+
owns: [
|
|
3395
|
+
'add-to-cart',
|
|
3396
|
+
'cart-link',
|
|
3397
|
+
'cart-lines'
|
|
3398
|
+
]
|
|
3399
|
+
}
|
|
3400
|
+
]
|
|
3401
|
+
}
|
|
3402
|
+
} : {},
|
|
3403
|
+
...appHasEffectApi(app) ? {
|
|
3404
|
+
effect: {
|
|
3405
|
+
runtime: 'effect',
|
|
3406
|
+
import: '@modern-js/plugin-bff/effect-edge',
|
|
3407
|
+
prefix: app.effectApi.prefix,
|
|
3408
|
+
openapi: '/openapi.json',
|
|
3409
|
+
workerEntry: 'worker/__modern_bff_effect.js',
|
|
3410
|
+
contract: './shared/effect/api',
|
|
3411
|
+
client: './effect/client',
|
|
3412
|
+
...createEffectOperationContract(app)
|
|
3413
|
+
}
|
|
3414
|
+
} : {}
|
|
3415
|
+
};
|
|
3416
|
+
}
|
|
3417
|
+
function createGeneratedContract(scope, apps = [
|
|
3418
|
+
shellApp,
|
|
3419
|
+
...remoteApps
|
|
3420
|
+
], enableTailwind = true) {
|
|
3421
|
+
return {
|
|
3422
|
+
schemaVersion: 1,
|
|
3423
|
+
profile: 'cloudflare-ssr-mf-effect-v1',
|
|
3424
|
+
packageManager: {
|
|
3425
|
+
source: 'package.json',
|
|
3426
|
+
manager: 'pnpm',
|
|
3427
|
+
version: PNPM_VERSION,
|
|
3428
|
+
toolchain: 'mise'
|
|
3429
|
+
},
|
|
3430
|
+
versions: {
|
|
3431
|
+
typescript: TYPESCRIPT_VERSION,
|
|
3432
|
+
typescriptNativePreview: TYPESCRIPT_NATIVE_PREVIEW_VERSION,
|
|
3433
|
+
moduleFederation: MODULE_FEDERATION_VERSION,
|
|
3434
|
+
tanstackRouter: TANSTACK_ROUTER_VERSION,
|
|
3435
|
+
i18next: I18NEXT_VERSION,
|
|
3436
|
+
zephyrRspackPlugin: ZEPHYR_RSPACK_PLUGIN_VERSION,
|
|
3437
|
+
zephyrAgent: ZEPHYR_AGENT_VERSION,
|
|
3438
|
+
wrangler: WRANGLER_VERSION
|
|
3439
|
+
},
|
|
3440
|
+
apps: apps.map((app)=>createAppGeneratedContract(scope, app, apps, enableTailwind))
|
|
3441
|
+
};
|
|
3442
|
+
}
|
|
1501
3443
|
function createTemplateManifest(modernVersion, packageSource) {
|
|
1502
3444
|
return {
|
|
1503
3445
|
schemaVersion: 1,
|
|
@@ -1532,11 +3474,18 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1532
3474
|
materialization: {
|
|
1533
3475
|
targetRoot: 'generated-project-root',
|
|
1534
3476
|
allowedPaths: [
|
|
3477
|
+
'.agents/**',
|
|
3478
|
+
'.github/**',
|
|
3479
|
+
'.gitignore',
|
|
3480
|
+
'.mise.toml',
|
|
1535
3481
|
'.modernjs/**',
|
|
3482
|
+
'AGENTS.md',
|
|
1536
3483
|
'README.md',
|
|
1537
3484
|
'apps/**',
|
|
1538
3485
|
'packages/**',
|
|
1539
3486
|
'package.json',
|
|
3487
|
+
'oxfmt.config.ts',
|
|
3488
|
+
'oxlint.config.ts',
|
|
1540
3489
|
'pnpm-workspace.yaml',
|
|
1541
3490
|
"scripts/**",
|
|
1542
3491
|
'services/**',
|
|
@@ -1545,7 +3494,6 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1545
3494
|
],
|
|
1546
3495
|
deniedPaths: [
|
|
1547
3496
|
'.git/**',
|
|
1548
|
-
'.github/**',
|
|
1549
3497
|
'.npmrc',
|
|
1550
3498
|
'.yarnrc',
|
|
1551
3499
|
'.env',
|
|
@@ -1561,6 +3509,28 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1561
3509
|
modernPackageSpecifier: modernPackageVersion(packageSource),
|
|
1562
3510
|
generatedWorkspacePackageSpecifier: WORKSPACE_PACKAGE_VERSION
|
|
1563
3511
|
},
|
|
3512
|
+
agentSkills: {
|
|
3513
|
+
installDir: '.agents/skills',
|
|
3514
|
+
source: {
|
|
3515
|
+
repository: 'https://github.com/rstackjs/agent-skills',
|
|
3516
|
+
commit: RSTACK_AGENT_SKILLS_COMMIT,
|
|
3517
|
+
license: 'MIT',
|
|
3518
|
+
licensePath: '.agents/rstackjs-agent-skills-LICENSE'
|
|
3519
|
+
},
|
|
3520
|
+
baseline: baselineAgentSkills,
|
|
3521
|
+
moduleFederationSource: {
|
|
3522
|
+
repository: 'https://github.com/module-federation/agent-skills',
|
|
3523
|
+
commit: MODULE_FEDERATION_AGENT_SKILLS_COMMIT,
|
|
3524
|
+
install: 'clone',
|
|
3525
|
+
baseline: moduleFederationAgentSkills
|
|
3526
|
+
},
|
|
3527
|
+
privateSource: {
|
|
3528
|
+
repository: 'https://github.com/TechsioCZ/skills',
|
|
3529
|
+
install: 'clone-if-authorized',
|
|
3530
|
+
baseline: privateAgentSkills
|
|
3531
|
+
},
|
|
3532
|
+
lockFile: '.agents/skills-lock.json'
|
|
3533
|
+
},
|
|
1564
3534
|
validation: {
|
|
1565
3535
|
schemaValidation: true,
|
|
1566
3536
|
sourceValidation: [
|
|
@@ -1577,48 +3547,82 @@ function createTemplateManifest(modernVersion, packageSource) {
|
|
|
1577
3547
|
],
|
|
1578
3548
|
postMaterializationValidation: [
|
|
1579
3549
|
'ultramodern-workspace-contract-check',
|
|
3550
|
+
'github-workflow-security-enforced',
|
|
3551
|
+
'pnpm-11-policy-enforced',
|
|
1580
3552
|
'template-manifest-retained'
|
|
1581
3553
|
],
|
|
1582
3554
|
expectedCommands: [
|
|
1583
|
-
|
|
1584
|
-
'pnpm
|
|
3555
|
+
'mise install',
|
|
3556
|
+
'mise exec -- pnpm install',
|
|
3557
|
+
'mise exec -- pnpm run ultramodern:check'
|
|
1585
3558
|
]
|
|
1586
3559
|
}
|
|
1587
3560
|
};
|
|
1588
3561
|
}
|
|
1589
|
-
function writeApp(targetDir, scope, app, packageSource) {
|
|
1590
|
-
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource));
|
|
1591
|
-
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory));
|
|
1592
|
-
writeFile(targetDir, `${app.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
3562
|
+
function writeApp(targetDir, scope, app, packageSource, enableTailwind) {
|
|
3563
|
+
writeJson(targetDir, `${app.directory}/package.json`, createAppPackage(scope, app, packageSource, enableTailwind));
|
|
3564
|
+
writeJson(targetDir, `${app.directory}/tsconfig.json`, createPackageTsConfig(app.directory, appHasEffectApi(app)));
|
|
3565
|
+
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");
|
|
3566
|
+
writeFile(targetDir, `${app.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, app));
|
|
1593
3567
|
writeFile(targetDir, `${app.directory}/modern.config.ts`, createAppModernConfig(app));
|
|
3568
|
+
writeFile(targetDir, `${app.directory}/src/modern.runtime.ts`, createAppRuntimeConfig(app));
|
|
3569
|
+
writeJson(targetDir, `${app.directory}/locales/en/translation.json`, createAppLocaleMessages(app, 'en'));
|
|
3570
|
+
writeJson(targetDir, `${app.directory}/locales/cs/translation.json`, createAppLocaleMessages(app, 'cs'));
|
|
3571
|
+
writeFile(targetDir, `${app.directory}/src/routes/index.css`, createAppStyles(enableTailwind));
|
|
3572
|
+
if (enableTailwind) {
|
|
3573
|
+
writeFile(targetDir, `${app.directory}/postcss.config.mjs`, createPostcssConfig());
|
|
3574
|
+
writeFile(targetDir, `${app.directory}/tailwind.config.ts`, createTailwindConfig());
|
|
3575
|
+
}
|
|
1594
3576
|
writeFile(targetDir, `${app.directory}/module-federation.config.ts`, 'shell' === app.kind ? createShellModuleFederationConfig() : createRemoteModuleFederationConfig(app));
|
|
1595
3577
|
writeFile(targetDir, `${app.directory}/src/routes/layout.tsx`, createLayout(app.id));
|
|
1596
|
-
writeFile(targetDir, `${app.directory}/src/routes/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
1597
|
-
if ('
|
|
3578
|
+
writeFile(targetDir, `${app.directory}/src/routes/[lang]/page.tsx`, 'shell' === app.kind ? createShellPage() : createRemotePage(app));
|
|
3579
|
+
if ('shell' === app.kind) writeFile(targetDir, `${app.directory}/src/effect/recommendations-client.ts`, createShellEffectClient(scope));
|
|
3580
|
+
if (appHasEffectApi(app)) {
|
|
3581
|
+
writeFile(targetDir, `${app.directory}/shared/effect/api.ts`, createEffectSharedApi(app));
|
|
3582
|
+
writeFile(targetDir, `${app.directory}/api/effect/index.ts`, createEffectServiceEntry(scope, app, '../../shared/effect/api'));
|
|
3583
|
+
writeFile(targetDir, `${app.directory}/src/effect/${app.effectApi.stem}-client.ts`, createEffectClient(app, '../../shared/effect/api'));
|
|
3584
|
+
}
|
|
3585
|
+
if ('vertical' === app.kind || 'horizontal-remote' === app.kind) {
|
|
1598
3586
|
writeFile(targetDir, `${app.directory}/src/remote-entry.tsx`, createRemoteEntry(app));
|
|
1599
|
-
|
|
1600
|
-
writeFile(targetDir, `${app.directory}/src/components/${widgetFile}`, createRemoteWidget(app));
|
|
3587
|
+
writeFile(targetDir, `${app.directory}/src/components/${remoteWidgetFile(app)}.tsx`, createRemoteWidget(app));
|
|
1601
3588
|
}
|
|
1602
3589
|
if ('horizontal-design-system' === app.kind) {
|
|
1603
|
-
writeFile(targetDir, `${app.directory}/src/components/
|
|
3590
|
+
writeFile(targetDir, `${app.directory}/src/components/button.tsx`, createDesignButton());
|
|
1604
3591
|
writeFile(targetDir, `${app.directory}/src/tokens.ts`, createDesignTokens());
|
|
1605
3592
|
}
|
|
1606
3593
|
}
|
|
1607
|
-
function writeEffectService(targetDir, scope, packageSource) {
|
|
1608
|
-
writeJson(targetDir, `${
|
|
1609
|
-
writeJson(targetDir, `${
|
|
1610
|
-
writeFile(targetDir, `${
|
|
1611
|
-
writeFile(targetDir, `${
|
|
1612
|
-
|
|
3594
|
+
function writeEffectService(targetDir, scope, packageSource, enableTailwind, service = effectService) {
|
|
3595
|
+
writeJson(targetDir, `${service.directory}/package.json`, createServicePackage(scope, packageSource, enableTailwind, service));
|
|
3596
|
+
writeJson(targetDir, `${service.directory}/tsconfig.json`, createPackageTsConfig(service.directory, true));
|
|
3597
|
+
writeFile(targetDir, `${service.directory}/src/modern-app-env.d.ts`, "/// <reference types='@modern-js/app-tools/types' />\n");
|
|
3598
|
+
writeFile(targetDir, `${service.directory}/src/ultramodern-build.ts`, createUltramodernBuildModule(scope, service));
|
|
3599
|
+
writeFile(targetDir, `${service.directory}/src/routes/layout.tsx`, createLayout(service.id));
|
|
3600
|
+
writeFile(targetDir, `${service.directory}/src/routes/page.tsx`, `export default function ${toPascalCase(service.id)}Home() {
|
|
3601
|
+
return <main>${service.id} Effect service</main>;
|
|
1613
3602
|
}
|
|
1614
3603
|
`);
|
|
1615
|
-
writeFile(targetDir, `${
|
|
1616
|
-
|
|
1617
|
-
|
|
3604
|
+
writeFile(targetDir, `${service.directory}/src/routes/index.css`, createAppStyles(enableTailwind));
|
|
3605
|
+
if (enableTailwind) {
|
|
3606
|
+
writeFile(targetDir, `${service.directory}/postcss.config.mjs`, createPostcssConfig());
|
|
3607
|
+
writeFile(targetDir, `${service.directory}/tailwind.config.ts`, createTailwindConfig());
|
|
3608
|
+
}
|
|
3609
|
+
writeFile(targetDir, `${service.directory}/modern.config.ts`, createServiceModernConfigFor(service));
|
|
3610
|
+
writeFile(targetDir, `${service.directory}/api/effect/index.ts`, createEffectServiceEntry(scope, service));
|
|
3611
|
+
}
|
|
3612
|
+
function writeGenericSharedPackage(targetDir, scope, packageSource, sharedPackage) {
|
|
3613
|
+
writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description, packageSource));
|
|
3614
|
+
writeJson(targetDir, `${sharedPackage.directory}/tsconfig.json`, {
|
|
3615
|
+
extends: `${relativeRootFor(sharedPackage.directory)}/tsconfig.base.json`,
|
|
3616
|
+
include: [
|
|
3617
|
+
'src'
|
|
3618
|
+
]
|
|
3619
|
+
});
|
|
3620
|
+
writeFile(targetDir, `${sharedPackage.directory}/src/index.ts`, `export const packageId = '${sharedPackage.id}';
|
|
3621
|
+
`);
|
|
1618
3622
|
}
|
|
1619
|
-
function writeSharedPackages(targetDir, scope) {
|
|
3623
|
+
function writeSharedPackages(targetDir, scope, packageSource) {
|
|
1620
3624
|
for (const sharedPackage of sharedPackages){
|
|
1621
|
-
writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description));
|
|
3625
|
+
writeJson(targetDir, `${sharedPackage.directory}/package.json`, createSharedPackage(scope, sharedPackage.id, sharedPackage.description, packageSource));
|
|
1622
3626
|
writeJson(targetDir, `${sharedPackage.directory}/tsconfig.json`, {
|
|
1623
3627
|
extends: `${relativeRootFor(sharedPackage.directory)}/tsconfig.base.json`,
|
|
1624
3628
|
include: [
|
|
@@ -1627,51 +3631,315 @@ function writeSharedPackages(targetDir, scope) {
|
|
|
1627
3631
|
});
|
|
1628
3632
|
}
|
|
1629
3633
|
writeFile(targetDir, 'packages/shared-contracts/src/index.ts', `export const ultramodernWorkspaceContract = {
|
|
3634
|
+
ownership: 'topology/ownership.json',
|
|
1630
3635
|
preset: 'presetUltramodern',
|
|
1631
3636
|
topology: 'topology/reference-topology.json',
|
|
1632
|
-
ownership: 'topology/ownership.json',
|
|
1633
3637
|
} as const;
|
|
1634
3638
|
`);
|
|
1635
3639
|
writeFile(targetDir, 'packages/shared-design-tokens/src/index.ts', `export const sharedDesignTokens = {
|
|
1636
3640
|
color: {
|
|
1637
|
-
surface: '#f6fbf7',
|
|
1638
|
-
foreground: '#133225',
|
|
1639
3641
|
accent: '#2f8f68',
|
|
3642
|
+
foreground: '#133225',
|
|
3643
|
+
surface: '#f6fbf7',
|
|
1640
3644
|
},
|
|
1641
3645
|
} as const;
|
|
1642
3646
|
`);
|
|
1643
|
-
writeFile(targetDir, 'packages/shared-effect-api/src/index.ts',
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
3647
|
+
writeFile(targetDir, 'packages/shared-effect-api/src/index.ts', createEffectSharedApi());
|
|
3648
|
+
}
|
|
3649
|
+
function readJsonFile(filePath) {
|
|
3650
|
+
return JSON.parse(node_fs.readFileSync(filePath, 'utf-8'));
|
|
3651
|
+
}
|
|
3652
|
+
function writeJsonFile(filePath, value) {
|
|
3653
|
+
node_fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf-8');
|
|
3654
|
+
}
|
|
3655
|
+
function appendEffectSharedApiContract(targetDir, service = effectService) {
|
|
3656
|
+
const relativePath = 'packages/shared-effect-api/src/index.ts';
|
|
3657
|
+
assertSafeRelativePath(relativePath);
|
|
3658
|
+
const filePath = node_path.join(targetDir, relativePath);
|
|
3659
|
+
ensureInsideRoot(targetDir, filePath);
|
|
3660
|
+
if (!node_fs.existsSync(filePath)) throw new Error(`Missing generated Effect API package: ${relativePath}`);
|
|
3661
|
+
const current = node_fs.readFileSync(filePath, 'utf-8');
|
|
3662
|
+
const apiExport = serviceEffectApiExport(service);
|
|
3663
|
+
if (current.includes(`export const ${apiExport} =`)) return;
|
|
3664
|
+
const contentWithImports = current.includes('@modern-js/plugin-bff/effect-client') ? current.trimEnd() : `${createEffectSharedApiImports()}\n${current.trimEnd()}`;
|
|
3665
|
+
node_fs.writeFileSync(filePath, `${contentWithImports}\n\n${createEffectSharedApiContract(service)}`, 'utf-8');
|
|
3666
|
+
}
|
|
3667
|
+
function existingPackageSource(workspaceRoot, modernVersion, packageSource) {
|
|
3668
|
+
if (packageSource) return resolvePackageSource({
|
|
3669
|
+
targetDir: workspaceRoot,
|
|
3670
|
+
packageName: node_path.basename(workspaceRoot),
|
|
3671
|
+
modernVersion,
|
|
3672
|
+
packageSource
|
|
3673
|
+
});
|
|
3674
|
+
const metadataPath = node_path.join(workspaceRoot, '.modernjs/ultramodern-package-source.json');
|
|
3675
|
+
if (!node_fs.existsSync(metadataPath)) return resolvePackageSource({
|
|
3676
|
+
targetDir: workspaceRoot,
|
|
3677
|
+
packageName: node_path.basename(workspaceRoot),
|
|
3678
|
+
modernVersion
|
|
3679
|
+
});
|
|
3680
|
+
const metadata = readJsonFile(metadataPath);
|
|
3681
|
+
const aliases = metadata.modernPackages?.aliases ?? {};
|
|
3682
|
+
const firstAlias = Object.values(aliases).find((value)=>'string' == typeof value);
|
|
3683
|
+
const firstPackage = Object.keys(aliases)[0];
|
|
3684
|
+
const aliasScope = firstAlias?.match(/^@([^/]+)\//)?.[1];
|
|
3685
|
+
const unscopedName = firstPackage?.split('/').at(-1) ?? '';
|
|
3686
|
+
const aliasUnscopedName = firstAlias?.split('/').at(-1) ?? '';
|
|
3687
|
+
const aliasPackageNamePrefix = aliasUnscopedName && unscopedName && aliasUnscopedName.endsWith(unscopedName) ? aliasUnscopedName.slice(0, -unscopedName.length) : void 0;
|
|
3688
|
+
return {
|
|
3689
|
+
strategy: 'install' === metadata.strategy ? 'install' : 'workspace',
|
|
3690
|
+
modernPackageVersion: 'string' == typeof metadata.modernPackages?.specifier ? metadata.modernPackages.specifier : modernVersion,
|
|
3691
|
+
registry: metadata.modernPackages?.registry,
|
|
3692
|
+
aliasScope,
|
|
3693
|
+
aliasPackageNamePrefix
|
|
3694
|
+
};
|
|
3695
|
+
}
|
|
3696
|
+
function assertValidMicroVerticalName(name) {
|
|
3697
|
+
const normalized = toKebabCase(name);
|
|
3698
|
+
if (!normalized || normalized !== name) throw new Error(`Invalid MicroVertical name "${name}". Use lowercase kebab-case.`);
|
|
3699
|
+
return normalized;
|
|
3700
|
+
}
|
|
3701
|
+
function nextAvailablePort(ports) {
|
|
3702
|
+
const numericPorts = Object.values(ports).filter((value)=>'number' == typeof value && Number.isFinite(value));
|
|
3703
|
+
return Math.max(3030, ...numericPorts) + 1;
|
|
3704
|
+
}
|
|
3705
|
+
function assertCanCreate(workspaceRoot, relativePath) {
|
|
3706
|
+
if (node_fs.existsSync(node_path.join(workspaceRoot, relativePath))) throw new Error(`Refusing to overwrite existing path: ${relativePath}`);
|
|
3707
|
+
}
|
|
3708
|
+
function addRootDevScript(workspaceRoot, scope, packageSuffix, scriptName) {
|
|
3709
|
+
const packagePath = node_path.join(workspaceRoot, 'package.json');
|
|
3710
|
+
const rootPackage = readJsonFile(packagePath);
|
|
3711
|
+
rootPackage.scripts ??= {};
|
|
3712
|
+
rootPackage.scripts[`dev:${scriptName}`] = `pnpm --filter ${ultramodern_workspace_packageName(scope, packageSuffix)} dev`;
|
|
3713
|
+
if ('string' == typeof rootPackage.scripts.dev && !rootPackage.scripts.dev.includes(ultramodern_workspace_packageName(scope, packageSuffix))) {
|
|
3714
|
+
const packageFilter = `--filter ${ultramodern_workspace_packageName(scope, packageSuffix)}`;
|
|
3715
|
+
rootPackage.scripts.dev = rootPackage.scripts.dev.endsWith(' dev') ? rootPackage.scripts.dev.replace(/ dev$/u, ` ${packageFilter} dev`) : `${rootPackage.scripts.dev} ${packageFilter}`;
|
|
3716
|
+
}
|
|
3717
|
+
writeJsonFile(packagePath, rootPackage);
|
|
3718
|
+
}
|
|
3719
|
+
function addShellZephyrDependency(workspaceRoot, scope, remote) {
|
|
3720
|
+
const packagePath = node_path.join(workspaceRoot, shellApp.directory, 'package.json');
|
|
3721
|
+
const shellPackage = readJsonFile(packagePath);
|
|
3722
|
+
shellPackage['zephyr:dependencies'] ??= {};
|
|
3723
|
+
shellPackage['zephyr:dependencies'][remoteDependencyAlias(remote)] = zephyrRemoteDependency(scope, remote);
|
|
3724
|
+
writeJsonFile(packagePath, shellPackage);
|
|
3725
|
+
}
|
|
3726
|
+
function addShellWorkspaceDependency(workspaceRoot, scope, remote) {
|
|
3727
|
+
if (!appHasEffectApi(remote)) return;
|
|
3728
|
+
const packagePath = node_path.join(workspaceRoot, shellApp.directory, 'package.json');
|
|
3729
|
+
const shellPackage = readJsonFile(packagePath);
|
|
3730
|
+
shellPackage.dependencies ??= {};
|
|
3731
|
+
shellPackage.dependencies[ultramodern_workspace_packageName(scope, remote.packageSuffix)] = WORKSPACE_PACKAGE_VERSION;
|
|
3732
|
+
writeJsonFile(packagePath, shellPackage);
|
|
3733
|
+
}
|
|
3734
|
+
function remoteTopologyEntry(scope, remote) {
|
|
3735
|
+
return {
|
|
3736
|
+
id: remote.id,
|
|
3737
|
+
kind: remote.kind,
|
|
3738
|
+
domain: remote.domain,
|
|
3739
|
+
package: ultramodern_workspace_packageName(scope, remote.packageSuffix),
|
|
3740
|
+
moduleFederation: {
|
|
3741
|
+
role: 'remote',
|
|
3742
|
+
name: remote.mfName,
|
|
3743
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`,
|
|
3744
|
+
exposes: Object.keys(remote.exposes ?? {}),
|
|
3745
|
+
ssr: true,
|
|
3746
|
+
fallbackTelemetryEvent: 'modernjs:mv-runtime-parity',
|
|
3747
|
+
sharedContractVersion: 'mf-ssr-contract-v1'
|
|
3748
|
+
},
|
|
3749
|
+
...effectApiTopologyMetadata(remote) ? {
|
|
3750
|
+
api: effectApiTopologyMetadata(remote)
|
|
3751
|
+
} : {},
|
|
3752
|
+
ownership: remote.ownership
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
function ownershipEntry(scope, owner) {
|
|
3756
|
+
return {
|
|
3757
|
+
id: owner.id,
|
|
3758
|
+
package: ultramodern_workspace_packageName(scope, owner.packageSuffix),
|
|
3759
|
+
path: owner.directory,
|
|
3760
|
+
ownership: owner.ownership
|
|
3761
|
+
};
|
|
3762
|
+
}
|
|
3763
|
+
function remotesFromTopology(topology, ports) {
|
|
3764
|
+
return (topology.remotes ?? []).map((remote)=>{
|
|
3765
|
+
const effectApi = remote.api?.effect ? {
|
|
3766
|
+
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-/, ''),
|
|
3767
|
+
prefix: remote.api.effect.bff?.prefix ?? `/${remote.domain ?? String(remote.id).replace(/^remote-/, '')}-api`,
|
|
3768
|
+
consumedBy: Array.isArray(remote.api.effect.consumedBy) ? remote.api.effect.consumedBy : [
|
|
3769
|
+
shellApp.id,
|
|
3770
|
+
remote.id
|
|
3771
|
+
]
|
|
3772
|
+
} : void 0;
|
|
3773
|
+
return {
|
|
3774
|
+
id: remote.id,
|
|
3775
|
+
directory: '',
|
|
3776
|
+
packageSuffix: remote.package?.split('/').at(-1) ?? remote.id,
|
|
3777
|
+
displayName: remote.id,
|
|
3778
|
+
kind: remote.kind ?? 'vertical',
|
|
3779
|
+
domain: remote.domain ?? String(remote.id).replace(/^remote-/, ''),
|
|
3780
|
+
portEnv: '',
|
|
3781
|
+
port: 'number' == typeof ports[remote.id] ? ports[remote.id] : 0,
|
|
3782
|
+
mfName: remote.moduleFederation?.name ?? `remote${toPascalCase(remote.id)}`,
|
|
3783
|
+
...effectApi ? {
|
|
3784
|
+
effectApi
|
|
3785
|
+
} : {},
|
|
3786
|
+
ownership: remote.ownership ?? createNeutralOwnership(remote.id)
|
|
3787
|
+
};
|
|
3788
|
+
});
|
|
3789
|
+
}
|
|
3790
|
+
function addUltramodernMicroVertical(options) {
|
|
3791
|
+
const name = assertValidMicroVerticalName(options.name);
|
|
3792
|
+
const rootPackage = readJsonFile(node_path.join(options.workspaceRoot, 'package.json'));
|
|
3793
|
+
const scope = toPackageScope(String(rootPackage.name ?? node_path.basename(options.workspaceRoot)));
|
|
3794
|
+
const topologyPath = node_path.join(options.workspaceRoot, 'topology/reference-topology.json');
|
|
3795
|
+
const ownershipPath = node_path.join(options.workspaceRoot, 'topology/ownership.json');
|
|
3796
|
+
const overlayPath = node_path.join(options.workspaceRoot, 'topology/local-overlays/development.json');
|
|
3797
|
+
for (const requiredPath of [
|
|
3798
|
+
topologyPath,
|
|
3799
|
+
ownershipPath,
|
|
3800
|
+
overlayPath
|
|
3801
|
+
])if (!node_fs.existsSync(requiredPath)) throw new Error(`Missing UltraModern workspace file: ${requiredPath}`);
|
|
3802
|
+
const topology = readJsonFile(topologyPath);
|
|
3803
|
+
const ownership = readJsonFile(ownershipPath);
|
|
3804
|
+
const overlay = readJsonFile(overlayPath);
|
|
3805
|
+
overlay.ports ??= {};
|
|
3806
|
+
const packageSource = existingPackageSource(options.workspaceRoot, options.modernVersion, options.packageSource);
|
|
3807
|
+
const enableTailwind = false !== options.enableTailwind;
|
|
3808
|
+
const port = nextAvailablePort(overlay.ports);
|
|
3809
|
+
if ('remote' === options.kind || 'horizontal-remote' === options.kind) {
|
|
3810
|
+
const remote = createRemoteDescriptor(name, options.kind, port);
|
|
3811
|
+
assertCanCreate(options.workspaceRoot, remote.directory);
|
|
3812
|
+
if ((topology.remotes ?? []).some((entry)=>entry.id === remote.id)) throw new Error(`Topology already contains ${remote.id}`);
|
|
3813
|
+
if (Object.values(overlay.ports).includes(remote.port)) throw new Error(`Development port ${remote.port} is already in use`);
|
|
3814
|
+
writeApp(options.workspaceRoot, scope, remote, packageSource, enableTailwind);
|
|
3815
|
+
topology.shell ??= {};
|
|
3816
|
+
topology.shell.remoteRefs ??= [];
|
|
3817
|
+
topology.shell.remoteRefs.push(remote.id);
|
|
3818
|
+
topology.shell.moduleFederation ??= {};
|
|
3819
|
+
topology.shell.moduleFederation.remotes ??= [];
|
|
3820
|
+
topology.shell.moduleFederation.remotes.push({
|
|
3821
|
+
id: remote.id,
|
|
3822
|
+
name: remote.mfName,
|
|
3823
|
+
manifestUrl: `http://localhost:${remote.port}/mf-manifest.json`
|
|
3824
|
+
});
|
|
3825
|
+
topology.remotes ??= [];
|
|
3826
|
+
topology.remotes.push(remoteTopologyEntry(scope, remote));
|
|
3827
|
+
ownership.owners ??= [];
|
|
3828
|
+
ownership.owners.push(ownershipEntry(scope, remote));
|
|
3829
|
+
overlay.ports[remote.id] = remote.port;
|
|
3830
|
+
overlay.manifests ??= {};
|
|
3831
|
+
overlay.manifests[remote.id] = `http://localhost:${remote.port}/mf-manifest.json`;
|
|
3832
|
+
if (appHasEffectApi(remote)) {
|
|
3833
|
+
overlay.apis ??= {};
|
|
3834
|
+
overlay.apis[remote.id] = `http://localhost:${remote.port}${effectApiPrefix(remote)}`;
|
|
3835
|
+
}
|
|
3836
|
+
writeJsonFile(topologyPath, topology);
|
|
3837
|
+
writeJsonFile(ownershipPath, ownership);
|
|
3838
|
+
writeJsonFile(overlayPath, overlay);
|
|
3839
|
+
writeJsonFile(node_path.join(options.workspaceRoot, GENERATED_CONTRACT_PATH), createGeneratedContract(scope, [
|
|
3840
|
+
shellApp,
|
|
3841
|
+
...remotesFromTopology(topology, overlay.ports)
|
|
3842
|
+
], enableTailwind));
|
|
3843
|
+
const shellConfigPath = node_path.join(options.workspaceRoot, `${shellApp.directory}/module-federation.config.ts`);
|
|
3844
|
+
writeFileReplacing(options.workspaceRoot, `${shellApp.directory}/module-federation.config.ts`, createShellModuleFederationConfig(remotesFromTopology(topology, overlay.ports)));
|
|
3845
|
+
if (!node_fs.existsSync(shellConfigPath)) throw new Error('Shell Module Federation config was not regenerated');
|
|
3846
|
+
addShellZephyrDependency(options.workspaceRoot, scope, remote);
|
|
3847
|
+
addShellWorkspaceDependency(options.workspaceRoot, scope, remote);
|
|
3848
|
+
addRootDevScript(options.workspaceRoot, scope, remote.packageSuffix, name);
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
if ('service' === options.kind) {
|
|
3852
|
+
const service = createServiceDescriptor(name, port);
|
|
3853
|
+
assertCanCreate(options.workspaceRoot, service.directory);
|
|
3854
|
+
if ((topology.effectServices ?? []).some((entry)=>entry.id === service.id)) throw new Error(`Topology already contains ${service.id}`);
|
|
3855
|
+
writeEffectService(options.workspaceRoot, scope, packageSource, enableTailwind, service);
|
|
3856
|
+
appendEffectSharedApiContract(options.workspaceRoot, service);
|
|
3857
|
+
topology.effectServices ??= [];
|
|
3858
|
+
topology.effectServices.push({
|
|
3859
|
+
id: service.id,
|
|
3860
|
+
kind: 'effect-service',
|
|
3861
|
+
runtime: 'effect',
|
|
3862
|
+
package: ultramodern_workspace_packageName(scope, service.packageSuffix),
|
|
3863
|
+
consumedBy: [
|
|
3864
|
+
shellApp.id
|
|
3865
|
+
],
|
|
3866
|
+
bff: {
|
|
3867
|
+
prefix: serviceApiPrefix(service),
|
|
3868
|
+
openapi: '/openapi.json'
|
|
3869
|
+
},
|
|
3870
|
+
contract: {
|
|
3871
|
+
package: ultramodern_workspace_packageName(scope, 'shared-effect-api'),
|
|
3872
|
+
export: serviceEffectApiExport(service),
|
|
3873
|
+
path: 'packages/shared-effect-api/src/index.ts'
|
|
3874
|
+
},
|
|
3875
|
+
serverEntry: `${service.directory}/api/effect/index.ts`,
|
|
3876
|
+
basePath: `${serviceApiPrefix(service)}/effect/${effectApiStem(service)}`,
|
|
3877
|
+
...createEffectOperationContract(service),
|
|
3878
|
+
ownership: service.ownership
|
|
3879
|
+
});
|
|
3880
|
+
ownership.owners ??= [];
|
|
3881
|
+
ownership.owners.push(ownershipEntry(scope, service));
|
|
3882
|
+
overlay.ports[service.id] = service.port;
|
|
3883
|
+
overlay.services ??= {};
|
|
3884
|
+
overlay.services[service.id] = `http://localhost:${service.port}${serviceApiPrefix(service)}`;
|
|
3885
|
+
writeJsonFile(topologyPath, topology);
|
|
3886
|
+
writeJsonFile(ownershipPath, ownership);
|
|
3887
|
+
writeJsonFile(overlayPath, overlay);
|
|
3888
|
+
addRootDevScript(options.workspaceRoot, scope, service.packageSuffix, name);
|
|
3889
|
+
return;
|
|
3890
|
+
}
|
|
3891
|
+
if ('shared' === options.kind) {
|
|
3892
|
+
const sharedPackage = createSharedPackageDescriptor(name);
|
|
3893
|
+
assertCanCreate(options.workspaceRoot, sharedPackage.directory);
|
|
3894
|
+
if ((topology.sharedPackages ?? []).some((entry)=>entry.id === sharedPackage.id)) throw new Error(`Topology already contains ${sharedPackage.id}`);
|
|
3895
|
+
writeGenericSharedPackage(options.workspaceRoot, scope, packageSource, sharedPackage);
|
|
3896
|
+
topology.sharedPackages ??= [];
|
|
3897
|
+
topology.sharedPackages.push({
|
|
3898
|
+
id: sharedPackage.id,
|
|
3899
|
+
package: ultramodern_workspace_packageName(scope, sharedPackage.id),
|
|
3900
|
+
path: sharedPackage.directory,
|
|
3901
|
+
description: sharedPackage.description
|
|
3902
|
+
});
|
|
3903
|
+
ownership.owners ??= [];
|
|
3904
|
+
ownership.owners.push(ownershipEntry(scope, {
|
|
3905
|
+
id: sharedPackage.id,
|
|
3906
|
+
packageSuffix: sharedPackage.id,
|
|
3907
|
+
directory: sharedPackage.directory,
|
|
3908
|
+
ownership: createNeutralOwnership(sharedPackage.id, 'tier-1-shared-contract')
|
|
3909
|
+
}));
|
|
3910
|
+
writeJsonFile(topologyPath, topology);
|
|
3911
|
+
writeJsonFile(ownershipPath, ownership);
|
|
3912
|
+
return;
|
|
3913
|
+
}
|
|
3914
|
+
throw new Error(`Unsupported MicroVertical kind: ${options.kind}`);
|
|
1653
3915
|
}
|
|
1654
3916
|
function generateUltramodernWorkspace(options) {
|
|
1655
3917
|
const scope = toPackageScope(options.packageName);
|
|
1656
3918
|
const packageSource = resolvePackageSource(options);
|
|
3919
|
+
const enableTailwind = false !== options.enableTailwind;
|
|
1657
3920
|
node_fs.mkdirSync(options.targetDir, {
|
|
1658
3921
|
recursive: true
|
|
1659
3922
|
});
|
|
1660
3923
|
copyRootTemplate(options.targetDir, {
|
|
1661
3924
|
packageName: options.packageName,
|
|
1662
|
-
packageScope: scope
|
|
3925
|
+
packageScope: scope,
|
|
3926
|
+
pnpmVersion: PNPM_VERSION,
|
|
3927
|
+
tailwindEnabled: String(enableTailwind)
|
|
1663
3928
|
});
|
|
1664
3929
|
writeJson(options.targetDir, 'package.json', createRootPackageJson(scope, packageSource));
|
|
1665
|
-
writeJson(options.targetDir, 'tsconfig.base.json', createTsConfigBase(
|
|
3930
|
+
writeJson(options.targetDir, 'tsconfig.base.json', createTsConfigBase());
|
|
1666
3931
|
writeJson(options.targetDir, 'topology/reference-topology.json', createTopology(scope));
|
|
1667
3932
|
writeJson(options.targetDir, 'topology/ownership.json', createOwnership(scope));
|
|
1668
3933
|
writeJson(options.targetDir, 'topology/local-overlays/development.json', createDevelopmentOverlay());
|
|
1669
3934
|
writeJson(options.targetDir, '.modernjs/ultramodern-workspace-template-manifest.json', createTemplateManifest(options.modernVersion, packageSource));
|
|
1670
3935
|
writeJson(options.targetDir, '.modernjs/ultramodern-package-source.json', createPackageSourceMetadata(scope, packageSource));
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
3936
|
+
writeJson(options.targetDir, GENERATED_CONTRACT_PATH, createGeneratedContract(scope, [
|
|
3937
|
+
shellApp,
|
|
3938
|
+
...remoteApps
|
|
3939
|
+
], enableTailwind));
|
|
3940
|
+
writeApp(options.targetDir, scope, shellApp, packageSource, enableTailwind);
|
|
3941
|
+
for (const remote of remoteApps)writeApp(options.targetDir, scope, remote, packageSource, enableTailwind);
|
|
3942
|
+
writeSharedPackages(options.targetDir, scope, packageSource);
|
|
1675
3943
|
}
|
|
1676
3944
|
const src_dirname = node_path.dirname(fileURLToPath(import.meta.url));
|
|
1677
3945
|
const templateDir = node_path.resolve(src_dirname, '..', 'template');
|
|
@@ -1681,9 +3949,12 @@ const sha1Pattern = /^[0-9a-f]{40}$/;
|
|
|
1681
3949
|
const sha256Pattern = /^[0-9a-f]{64}$/;
|
|
1682
3950
|
const templateIdPattern = /^[a-z0-9][a-z0-9._-]*$/;
|
|
1683
3951
|
const packageNamePattern = /^(?:@[a-z0-9._-]+\/)?[a-z0-9._-]+$/;
|
|
3952
|
+
const src_TANSTACK_ROUTER_VERSION = '1.170.8';
|
|
3953
|
+
const src_TAILWIND_VERSION = '4.3.0';
|
|
3954
|
+
const src_TAILWIND_POSTCSS_VERSION = '4.3.0';
|
|
3955
|
+
const src_PNPM_VERSION = '11.4.0';
|
|
1684
3956
|
const requiredDeniedPaths = [
|
|
1685
3957
|
'.git/**',
|
|
1686
|
-
'.github/**',
|
|
1687
3958
|
'.npmrc',
|
|
1688
3959
|
'.yarnrc',
|
|
1689
3960
|
'.env',
|
|
@@ -1726,8 +3997,8 @@ function detectRouterFramework() {
|
|
|
1726
3997
|
'--router',
|
|
1727
3998
|
'-r'
|
|
1728
3999
|
]);
|
|
1729
|
-
if (!routerValue || '
|
|
1730
|
-
if ('
|
|
4000
|
+
if (!routerValue || 'tanstack' === routerValue) return 'tanstack';
|
|
4001
|
+
if ('react-router' === routerValue) return 'react-router';
|
|
1731
4002
|
console.error(i18n.t(localeKeys.error.invalidRouter, {
|
|
1732
4003
|
router: routerValue
|
|
1733
4004
|
}));
|
|
@@ -1746,7 +4017,7 @@ function detectBffRuntime() {
|
|
|
1746
4017
|
process.exit(1);
|
|
1747
4018
|
}
|
|
1748
4019
|
function src_renderTemplate(template, data) {
|
|
1749
|
-
const tagRegex = /\{\{(#if|#unless|\/if|\/unless)(?:\s+(\w+))
|
|
4020
|
+
const tagRegex = /\{\{(~?)(#if|#unless|\/if|\/unless)(?:\s+(\w+))?(~?)\}\}/g;
|
|
1750
4021
|
function renderConditionals(startIndex, expectedClose) {
|
|
1751
4022
|
let rendered = '';
|
|
1752
4023
|
let cursor = startIndex;
|
|
@@ -1757,7 +4028,7 @@ function src_renderTemplate(template, data) {
|
|
|
1757
4028
|
rendered: rendered + template.slice(cursor),
|
|
1758
4029
|
nextIndex: template.length
|
|
1759
4030
|
};
|
|
1760
|
-
const [raw, tag, condition] = match;
|
|
4031
|
+
const [raw, , tag, condition, rightTrim] = match;
|
|
1761
4032
|
const tagIndex = match.index;
|
|
1762
4033
|
rendered += template.slice(cursor, tagIndex);
|
|
1763
4034
|
cursor = tagIndex + raw.length;
|
|
@@ -1773,10 +4044,17 @@ function src_renderTemplate(template, data) {
|
|
|
1773
4044
|
}
|
|
1774
4045
|
if ('/if' === tag || '/unless' === tag) {
|
|
1775
4046
|
const kind = '/if' === tag ? 'if' : 'unless';
|
|
1776
|
-
if (expectedClose === kind)
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
4047
|
+
if (expectedClose === kind) {
|
|
4048
|
+
let nextIndex = cursor;
|
|
4049
|
+
if ('~' === rightTrim) {
|
|
4050
|
+
const trailingWhitespace = /^\s*/u.exec(template.slice(nextIndex));
|
|
4051
|
+
nextIndex += trailingWhitespace?.[0].length ?? 0;
|
|
4052
|
+
}
|
|
4053
|
+
return {
|
|
4054
|
+
rendered,
|
|
4055
|
+
nextIndex
|
|
4056
|
+
};
|
|
4057
|
+
}
|
|
1780
4058
|
rendered += raw;
|
|
1781
4059
|
}
|
|
1782
4060
|
}
|
|
@@ -1857,20 +4135,29 @@ function createBuiltinTemplateManifest(version) {
|
|
|
1857
4135
|
materialization: {
|
|
1858
4136
|
targetRoot: 'generated-project-root',
|
|
1859
4137
|
allowedPaths: [
|
|
4138
|
+
'.agents/**',
|
|
1860
4139
|
'.browserslistrc',
|
|
4140
|
+
'.github/**',
|
|
1861
4141
|
'.gitignore',
|
|
4142
|
+
'.mise.toml',
|
|
1862
4143
|
'.modernjs/**',
|
|
1863
4144
|
'.nvmrc',
|
|
4145
|
+
'AGENTS.md',
|
|
1864
4146
|
'README.md',
|
|
1865
4147
|
'api/**',
|
|
1866
|
-
'
|
|
4148
|
+
'config/**',
|
|
1867
4149
|
'modern.config.ts',
|
|
4150
|
+
'oxfmt.config.ts',
|
|
4151
|
+
'oxlint.config.ts',
|
|
1868
4152
|
'package.json',
|
|
4153
|
+
'pnpm-workspace.yaml',
|
|
1869
4154
|
'postcss.config.mjs',
|
|
4155
|
+
'rstest.config.mts',
|
|
1870
4156
|
"scripts/**",
|
|
1871
4157
|
'shared/**',
|
|
1872
4158
|
'src/**',
|
|
1873
4159
|
'tailwind.config.ts',
|
|
4160
|
+
'tests/**',
|
|
1874
4161
|
'tsconfig.json'
|
|
1875
4162
|
],
|
|
1876
4163
|
deniedPaths: requiredDeniedPaths,
|
|
@@ -1899,11 +4186,17 @@ function createBuiltinTemplateManifest(version) {
|
|
|
1899
4186
|
postMaterializationValidation: [
|
|
1900
4187
|
'ultramodern-contract-check',
|
|
1901
4188
|
'dependency-install-with-lifecycle-deny',
|
|
4189
|
+
'github-workflow-security-enforced',
|
|
4190
|
+
'package-source-retained',
|
|
4191
|
+
'pnpm-11-policy-enforced',
|
|
4192
|
+
'rstest-smoke-tests',
|
|
1902
4193
|
'template-manifest-retained'
|
|
1903
4194
|
],
|
|
1904
4195
|
expectedCommands: [
|
|
1905
|
-
|
|
1906
|
-
'pnpm
|
|
4196
|
+
'mise install',
|
|
4197
|
+
'mise exec -- pnpm install',
|
|
4198
|
+
'mise exec -- pnpm test',
|
|
4199
|
+
'mise exec -- pnpm run ultramodern:check'
|
|
1907
4200
|
]
|
|
1908
4201
|
}
|
|
1909
4202
|
};
|
|
@@ -2008,9 +4301,19 @@ function writeTemplateManifestEvidence(targetDir, manifest) {
|
|
|
2008
4301
|
});
|
|
2009
4302
|
node_fs.writeFileSync(evidencePath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
2010
4303
|
}
|
|
2011
|
-
function
|
|
4304
|
+
function readCreatePackageJson() {
|
|
2012
4305
|
const createPackageJson = node_path.resolve(src_dirname, '..', 'package.json');
|
|
2013
|
-
|
|
4306
|
+
return JSON.parse(node_fs.readFileSync(createPackageJson, 'utf-8'));
|
|
4307
|
+
}
|
|
4308
|
+
function isBleedingDevCreatePackage(createPackage) {
|
|
4309
|
+
return '@bleedingdev/modern-js-create' === createPackage.name;
|
|
4310
|
+
}
|
|
4311
|
+
function getBleedingDevFrameworkVersion(createPackage, fallbackVersion) {
|
|
4312
|
+
const frameworkVersion = createPackage.ultramodern?.frameworkVersion;
|
|
4313
|
+
return 'string' == typeof frameworkVersion && frameworkVersion.length > 0 ? frameworkVersion : fallbackVersion;
|
|
4314
|
+
}
|
|
4315
|
+
function showVersion() {
|
|
4316
|
+
const createPackage = readCreatePackageJson();
|
|
2014
4317
|
const version = createPackage.version || 'unknown';
|
|
2015
4318
|
console.log(i18n.t(localeKeys.version.message, {
|
|
2016
4319
|
version
|
|
@@ -2037,6 +4340,7 @@ function showHelp() {
|
|
|
2037
4340
|
if (localeKeys.help.optionUltramodernPackageSource) console.log(i18n.t(localeKeys.help.optionUltramodernPackageSource));
|
|
2038
4341
|
if (localeKeys.help.optionUltramodernPackageScope) console.log(i18n.t(localeKeys.help.optionUltramodernPackageScope));
|
|
2039
4342
|
if (localeKeys.help.optionUltramodernPackageNamePrefix) console.log(i18n.t(localeKeys.help.optionUltramodernPackageNamePrefix));
|
|
4343
|
+
if (localeKeys.help.optionMicroVertical) console.log(i18n.t(localeKeys.help.optionMicroVertical));
|
|
2040
4344
|
console.log(i18n.t(localeKeys.help.optionSub));
|
|
2041
4345
|
console.log('');
|
|
2042
4346
|
console.log(i18n.t(localeKeys.help.examples));
|
|
@@ -2050,6 +4354,7 @@ function showHelp() {
|
|
|
2050
4354
|
if (localeKeys.help.example8) console.log(i18n.t(localeKeys.help.example8));
|
|
2051
4355
|
if (localeKeys.help.example9) console.log(i18n.t(localeKeys.help.example9));
|
|
2052
4356
|
if (localeKeys.help.example10) console.log(i18n.t(localeKeys.help.example10));
|
|
4357
|
+
if (localeKeys.help.example11) console.log(i18n.t(localeKeys.help.example11));
|
|
2053
4358
|
console.log('');
|
|
2054
4359
|
console.log(i18n.t(localeKeys.help.moreInfo));
|
|
2055
4360
|
console.log('');
|
|
@@ -2075,20 +4380,30 @@ function detectSubprojectFlag() {
|
|
|
2075
4380
|
}
|
|
2076
4381
|
function detectTailwindFlag() {
|
|
2077
4382
|
const args = process.argv.slice(2);
|
|
2078
|
-
return args.includes('--tailwind');
|
|
4383
|
+
return !args.includes('--no-tailwind');
|
|
2079
4384
|
}
|
|
2080
4385
|
function detectWorkspaceProtocolFlag() {
|
|
2081
4386
|
const args = process.argv.slice(2);
|
|
2082
4387
|
return args.includes('--workspace');
|
|
2083
4388
|
}
|
|
2084
|
-
function
|
|
4389
|
+
function detectMicroVerticalKind() {
|
|
4390
|
+
const kind = getOptionValue(process.argv.slice(2), [
|
|
4391
|
+
'--microvertical'
|
|
4392
|
+
]);
|
|
4393
|
+
if (!kind) return;
|
|
4394
|
+
if ('remote' === kind || 'horizontal-remote' === kind || 'service' === kind || 'shared' === kind) return kind;
|
|
4395
|
+
console.error('--microvertical must be one of: remote, horizontal-remote, service, shared');
|
|
4396
|
+
process.exit(1);
|
|
4397
|
+
}
|
|
4398
|
+
function detectUltramodernWorkspaceFlag(createPackage) {
|
|
2085
4399
|
const args = process.argv.slice(2);
|
|
2086
|
-
return args.includes(ULTRAMODERN_WORKSPACE_FLAG);
|
|
4400
|
+
return args.includes(ULTRAMODERN_WORKSPACE_FLAG) || isBleedingDevCreatePackage(createPackage);
|
|
2087
4401
|
}
|
|
2088
|
-
function detectUltramodernPackageSource(args,
|
|
4402
|
+
function detectUltramodernPackageSource(args, defaultPackageVersion, createPackage) {
|
|
4403
|
+
const bleedingDevDefaults = isBleedingDevCreatePackage(createPackage);
|
|
2089
4404
|
const strategy = getOptionValue(args, [
|
|
2090
4405
|
'--ultramodern-package-source'
|
|
2091
|
-
]) ?? 'workspace';
|
|
4406
|
+
]) ?? (bleedingDevDefaults ? 'install' : 'workspace');
|
|
2092
4407
|
if ('workspace' !== strategy && 'install' !== strategy) {
|
|
2093
4408
|
console.error('--ultramodern-package-source must be "workspace" or "install"');
|
|
2094
4409
|
process.exit(1);
|
|
@@ -2097,18 +4412,68 @@ function detectUltramodernPackageSource(args, modernVersion) {
|
|
|
2097
4412
|
strategy,
|
|
2098
4413
|
modernPackageVersion: getOptionValue(args, [
|
|
2099
4414
|
'--ultramodern-package-version'
|
|
2100
|
-
]) ??
|
|
4415
|
+
]) ?? defaultPackageVersion,
|
|
2101
4416
|
registry: getOptionValue(args, [
|
|
2102
4417
|
'--ultramodern-package-registry'
|
|
2103
4418
|
]),
|
|
2104
4419
|
aliasScope: getOptionValue(args, [
|
|
2105
4420
|
'--ultramodern-package-scope'
|
|
2106
|
-
]),
|
|
4421
|
+
]) ?? (bleedingDevDefaults && 'install' === strategy ? 'bleedingdev' : void 0),
|
|
2107
4422
|
aliasPackageNamePrefix: getOptionValue(args, [
|
|
2108
4423
|
'--ultramodern-package-name-prefix'
|
|
2109
4424
|
]) ?? 'modern-js-'
|
|
2110
4425
|
};
|
|
2111
4426
|
}
|
|
4427
|
+
function src_modernAliasPackageName(packageName, packageSource) {
|
|
4428
|
+
if (!packageSource.aliasScope) return packageName;
|
|
4429
|
+
const scope = packageSource.aliasScope.replace(/^@/, '');
|
|
4430
|
+
const unscopedName = packageName.split('/').at(-1);
|
|
4431
|
+
return `@${scope}/${packageSource.aliasPackageNamePrefix ?? ''}${unscopedName}`;
|
|
4432
|
+
}
|
|
4433
|
+
function singleAppModernPackageSpecifier(packageName, packageSource, useWorkspaceProtocol) {
|
|
4434
|
+
if (useWorkspaceProtocol) return 'workspace:*';
|
|
4435
|
+
if ('install' !== packageSource.strategy || !packageSource.aliasScope) return packageSource.modernPackageVersion;
|
|
4436
|
+
return `npm:${src_modernAliasPackageName(packageName, packageSource)}@${packageSource.modernPackageVersion}`;
|
|
4437
|
+
}
|
|
4438
|
+
const singleAppModernPackages = [
|
|
4439
|
+
'@modern-js/runtime',
|
|
4440
|
+
'@modern-js/app-tools',
|
|
4441
|
+
'@modern-js/tsconfig',
|
|
4442
|
+
'@modern-js/plugin-i18n',
|
|
4443
|
+
'@modern-js/plugin-tanstack',
|
|
4444
|
+
'@modern-js/plugin-bff',
|
|
4445
|
+
'@modern-js/adapter-rstest'
|
|
4446
|
+
];
|
|
4447
|
+
function createSingleAppPackageSourceEvidence(packageSource, useWorkspaceProtocol) {
|
|
4448
|
+
const strategy = useWorkspaceProtocol ? 'workspace' : 'install';
|
|
4449
|
+
const specifier = useWorkspaceProtocol ? 'workspace:*' : packageSource.modernPackageVersion;
|
|
4450
|
+
const aliases = 'install' === strategy && packageSource.aliasScope ? Object.fromEntries(singleAppModernPackages.map((packageName)=>[
|
|
4451
|
+
packageName,
|
|
4452
|
+
src_modernAliasPackageName(packageName, packageSource)
|
|
4453
|
+
])) : void 0;
|
|
4454
|
+
return {
|
|
4455
|
+
schemaVersion: 1,
|
|
4456
|
+
preset: 'presetUltramodern',
|
|
4457
|
+
strategy,
|
|
4458
|
+
modernPackages: {
|
|
4459
|
+
specifier,
|
|
4460
|
+
packages: singleAppModernPackages,
|
|
4461
|
+
...packageSource.registry ? {
|
|
4462
|
+
registry: packageSource.registry
|
|
4463
|
+
} : {},
|
|
4464
|
+
...aliases ? {
|
|
4465
|
+
aliases
|
|
4466
|
+
} : {}
|
|
4467
|
+
}
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
function writeSingleAppPackageSourceEvidence(targetDir, packageSource, useWorkspaceProtocol) {
|
|
4471
|
+
const evidencePath = node_path.join(targetDir, '.modernjs', 'ultramodern-package-source.json');
|
|
4472
|
+
node_fs.mkdirSync(node_path.dirname(evidencePath), {
|
|
4473
|
+
recursive: true
|
|
4474
|
+
});
|
|
4475
|
+
node_fs.writeFileSync(evidencePath, `${JSON.stringify(createSingleAppPackageSourceEvidence(packageSource, useWorkspaceProtocol), null, 2)}\n`);
|
|
4476
|
+
}
|
|
2112
4477
|
function isDirectoryEmpty(dirPath) {
|
|
2113
4478
|
if (!node_fs.existsSync(dirPath)) return false;
|
|
2114
4479
|
try {
|
|
@@ -2130,7 +4495,8 @@ async function getProjectName() {
|
|
|
2130
4495
|
'--ultramodern-package-version',
|
|
2131
4496
|
'--ultramodern-package-registry',
|
|
2132
4497
|
'--ultramodern-package-scope',
|
|
2133
|
-
'--ultramodern-package-name-prefix'
|
|
4498
|
+
'--ultramodern-package-name-prefix',
|
|
4499
|
+
'--microvertical'
|
|
2134
4500
|
]);
|
|
2135
4501
|
const optionWithoutValue = new Set([
|
|
2136
4502
|
'--help',
|
|
@@ -2143,6 +4509,7 @@ async function getProjectName() {
|
|
|
2143
4509
|
'--tanstack',
|
|
2144
4510
|
'--bff',
|
|
2145
4511
|
'--tailwind',
|
|
4512
|
+
'--no-tailwind',
|
|
2146
4513
|
'--workspace',
|
|
2147
4514
|
ULTRAMODERN_WORKSPACE_FLAG
|
|
2148
4515
|
]);
|
|
@@ -2154,7 +4521,7 @@ async function getProjectName() {
|
|
|
2154
4521
|
i += 1;
|
|
2155
4522
|
continue;
|
|
2156
4523
|
}
|
|
2157
|
-
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);
|
|
4524
|
+
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);
|
|
2158
4525
|
}
|
|
2159
4526
|
}
|
|
2160
4527
|
const projectNameArg = positionalArgs[0];
|
|
@@ -2185,6 +4552,27 @@ async function main() {
|
|
|
2185
4552
|
const { name: projectName, useCurrentDir } = await getProjectName();
|
|
2186
4553
|
const targetDir = useCurrentDir ? process.cwd() : node_path.isAbsolute(projectName) ? projectName : node_path.resolve(process.cwd(), projectName);
|
|
2187
4554
|
const generatedPackageName = useCurrentDir || node_path.isAbsolute(projectName) ? node_path.basename(targetDir) : projectName;
|
|
4555
|
+
const createPackage = readCreatePackageJson();
|
|
4556
|
+
const version = createPackage.version || 'latest';
|
|
4557
|
+
const ultramodernPackageVersion = isBleedingDevCreatePackage(createPackage) ? getBleedingDevFrameworkVersion(createPackage, version) : version;
|
|
4558
|
+
const microVerticalKind = detectMicroVerticalKind();
|
|
4559
|
+
if (microVerticalKind) {
|
|
4560
|
+
const overridePackageSource = args.some((arg)=>arg.startsWith('--ultramodern-package-')) ? detectUltramodernPackageSource(args, ultramodernPackageVersion, createPackage) : void 0;
|
|
4561
|
+
addUltramodernMicroVertical({
|
|
4562
|
+
workspaceRoot: process.cwd(),
|
|
4563
|
+
name: generatedPackageName,
|
|
4564
|
+
kind: microVerticalKind,
|
|
4565
|
+
modernVersion: version,
|
|
4566
|
+
enableTailwind: detectTailwindFlag(),
|
|
4567
|
+
packageSource: overridePackageSource
|
|
4568
|
+
});
|
|
4569
|
+
const dim = '\x1b[2m\x1b[3m';
|
|
4570
|
+
const reset = '\x1b[0m';
|
|
4571
|
+
console.log(`${i18n.t(localeKeys.message.success)}\n`);
|
|
4572
|
+
console.log(`${dim} mise install${reset}`);
|
|
4573
|
+
console.log(`${dim} mise exec -- pnpm ultramodern:check${reset}\n`);
|
|
4574
|
+
return;
|
|
4575
|
+
}
|
|
2188
4576
|
if (node_fs.existsSync(targetDir)) {
|
|
2189
4577
|
const files = node_fs.readdirSync(targetDir);
|
|
2190
4578
|
if (files.length > 0) {
|
|
@@ -2194,16 +4582,14 @@ async function main() {
|
|
|
2194
4582
|
process.exit(1);
|
|
2195
4583
|
}
|
|
2196
4584
|
}
|
|
2197
|
-
const
|
|
2198
|
-
const createPackage = JSON.parse(node_fs.readFileSync(createPackageJson, 'utf-8'));
|
|
2199
|
-
const version = createPackage.version || 'latest';
|
|
2200
|
-
const generateWorkspace = detectUltramodernWorkspaceFlag();
|
|
4585
|
+
const generateWorkspace = detectUltramodernWorkspaceFlag(createPackage);
|
|
2201
4586
|
if (generateWorkspace) {
|
|
2202
4587
|
generateUltramodernWorkspace({
|
|
2203
4588
|
targetDir,
|
|
2204
4589
|
packageName: generatedPackageName,
|
|
2205
4590
|
modernVersion: version,
|
|
2206
|
-
|
|
4591
|
+
enableTailwind: detectTailwindFlag(),
|
|
4592
|
+
packageSource: detectUltramodernPackageSource(args, ultramodernPackageVersion, createPackage)
|
|
2207
4593
|
});
|
|
2208
4594
|
const dim = '\x1b[2m\x1b[3m';
|
|
2209
4595
|
const reset = '\x1b[0m';
|
|
@@ -2212,8 +4598,9 @@ async function main() {
|
|
|
2212
4598
|
if (!useCurrentDir) console.log(`${dim} ${i18n.t(localeKeys.message.step1, {
|
|
2213
4599
|
projectName
|
|
2214
4600
|
})}${reset}`);
|
|
4601
|
+
console.log(`${dim} mise install${reset}`);
|
|
2215
4602
|
console.log(`${dim} ${i18n.t(localeKeys.message.step2)}${reset}`);
|
|
2216
|
-
console.log(`${dim} pnpm ultramodern:check${reset}`);
|
|
4603
|
+
console.log(`${dim} mise exec -- pnpm ultramodern:check${reset}`);
|
|
2217
4604
|
console.log(`${dim} ${i18n.t(localeKeys.message.step3)}${reset}\n`);
|
|
2218
4605
|
return;
|
|
2219
4606
|
}
|
|
@@ -2223,12 +4610,23 @@ async function main() {
|
|
|
2223
4610
|
const bffRuntime = detectBffRuntime();
|
|
2224
4611
|
const enableTailwind = detectTailwindFlag();
|
|
2225
4612
|
const useWorkspaceProtocol = detectWorkspaceProtocolFlag();
|
|
2226
|
-
const
|
|
4613
|
+
const packageSource = detectUltramodernPackageSource(args, ultramodernPackageVersion, createPackage);
|
|
2227
4614
|
const templateManifest = createBuiltinTemplateManifest(version);
|
|
2228
4615
|
validateTemplateManifest(templateManifest);
|
|
2229
4616
|
copyTemplate(templateDir, targetDir, {
|
|
2230
4617
|
packageName: generatedPackageName,
|
|
2231
|
-
version:
|
|
4618
|
+
version: useWorkspaceProtocol ? 'workspace:*' : packageSource.modernPackageVersion,
|
|
4619
|
+
runtimeVersion: singleAppModernPackageSpecifier('@modern-js/runtime', packageSource, useWorkspaceProtocol),
|
|
4620
|
+
appToolsVersion: singleAppModernPackageSpecifier('@modern-js/app-tools', packageSource, useWorkspaceProtocol),
|
|
4621
|
+
adapterRstestVersion: singleAppModernPackageSpecifier('@modern-js/adapter-rstest', packageSource, useWorkspaceProtocol),
|
|
4622
|
+
tsconfigVersion: singleAppModernPackageSpecifier('@modern-js/tsconfig', packageSource, useWorkspaceProtocol),
|
|
4623
|
+
pluginTanstackVersion: singleAppModernPackageSpecifier('@modern-js/plugin-tanstack', packageSource, useWorkspaceProtocol),
|
|
4624
|
+
pluginBffVersion: singleAppModernPackageSpecifier('@modern-js/plugin-bff', packageSource, useWorkspaceProtocol),
|
|
4625
|
+
pluginI18nVersion: singleAppModernPackageSpecifier('@modern-js/plugin-i18n', packageSource, useWorkspaceProtocol),
|
|
4626
|
+
tanstackRouterVersion: src_TANSTACK_ROUTER_VERSION,
|
|
4627
|
+
tailwindVersion: src_TAILWIND_VERSION,
|
|
4628
|
+
tailwindPostcssVersion: src_TAILWIND_POSTCSS_VERSION,
|
|
4629
|
+
pnpmVersion: src_PNPM_VERSION,
|
|
2232
4630
|
isSubproject,
|
|
2233
4631
|
routerFramework,
|
|
2234
4632
|
bffRuntime,
|
|
@@ -2238,21 +4636,37 @@ async function main() {
|
|
|
2238
4636
|
const targetPackageJson = node_path.join(targetDir, 'package.json');
|
|
2239
4637
|
const packageJson = JSON.parse(node_fs.readFileSync(targetPackageJson, 'utf-8'));
|
|
2240
4638
|
packageJson.name = generatedPackageName;
|
|
4639
|
+
packageJson.modernjs = {
|
|
4640
|
+
...packageJson.modernjs ?? {},
|
|
4641
|
+
preset: 'presetUltramodern',
|
|
4642
|
+
packageSource: {
|
|
4643
|
+
strategy: useWorkspaceProtocol ? 'workspace' : 'install',
|
|
4644
|
+
config: './.modernjs/ultramodern-package-source.json'
|
|
4645
|
+
}
|
|
4646
|
+
};
|
|
2241
4647
|
if (isSubproject) {
|
|
2242
4648
|
delete packageJson['lint-staged'];
|
|
2243
4649
|
delete packageJson['simple-git-hooks'];
|
|
2244
4650
|
if (packageJson.scripts) {
|
|
2245
4651
|
delete packageJson.scripts.prepare;
|
|
4652
|
+
delete packageJson.scripts.format;
|
|
4653
|
+
delete packageJson.scripts['format:check'];
|
|
2246
4654
|
delete packageJson.scripts.lint;
|
|
4655
|
+
delete packageJson.scripts['lint:fix'];
|
|
4656
|
+
delete packageJson.scripts['skills:install'];
|
|
4657
|
+
delete packageJson.scripts['skills:check'];
|
|
2247
4658
|
}
|
|
2248
4659
|
if (packageJson.devDependencies) {
|
|
2249
4660
|
delete packageJson.devDependencies['lint-staged'];
|
|
2250
4661
|
delete packageJson.devDependencies['simple-git-hooks'];
|
|
2251
|
-
delete packageJson.devDependencies
|
|
4662
|
+
delete packageJson.devDependencies.oxlint;
|
|
4663
|
+
delete packageJson.devDependencies.oxfmt;
|
|
4664
|
+
delete packageJson.devDependencies.ultracite;
|
|
2252
4665
|
}
|
|
2253
4666
|
}
|
|
2254
4667
|
node_fs.writeFileSync(targetPackageJson, `${JSON.stringify(packageJson, null, 2)}\n`);
|
|
2255
4668
|
writeTemplateManifestEvidence(targetDir, templateManifest);
|
|
4669
|
+
writeSingleAppPackageSourceEvidence(targetDir, packageSource, useWorkspaceProtocol);
|
|
2256
4670
|
const dim = '\x1b[2m\x1b[3m';
|
|
2257
4671
|
const reset = '\x1b[0m';
|
|
2258
4672
|
console.log(`${i18n.t(localeKeys.message.success)}\n`);
|
|
@@ -2268,10 +4682,14 @@ function copyTemplate(src, dest, options) {
|
|
|
2268
4682
|
recursive: true
|
|
2269
4683
|
});
|
|
2270
4684
|
const excludeInSubproject = [
|
|
4685
|
+
'.agents',
|
|
4686
|
+
'.github',
|
|
2271
4687
|
'.gitignore.handlebars',
|
|
2272
|
-
'
|
|
4688
|
+
'AGENTS.md',
|
|
2273
4689
|
'.npmrc',
|
|
2274
|
-
'.nvmrc'
|
|
4690
|
+
'.nvmrc',
|
|
4691
|
+
'oxfmt.config.ts',
|
|
4692
|
+
'oxlint.config.ts'
|
|
2275
4693
|
];
|
|
2276
4694
|
function copyRecursive(srcDir, destDir) {
|
|
2277
4695
|
const entries = node_fs.readdirSync(srcDir, {
|
|
@@ -2293,6 +4711,17 @@ function copyTemplate(src, dest, options) {
|
|
|
2293
4711
|
const rendered = src_renderTemplate(templateContent, {
|
|
2294
4712
|
packageName: options.packageName,
|
|
2295
4713
|
version: options.version,
|
|
4714
|
+
runtimeVersion: options.runtimeVersion,
|
|
4715
|
+
appToolsVersion: options.appToolsVersion,
|
|
4716
|
+
adapterRstestVersion: options.adapterRstestVersion,
|
|
4717
|
+
tsconfigVersion: options.tsconfigVersion,
|
|
4718
|
+
pluginTanstackVersion: options.pluginTanstackVersion,
|
|
4719
|
+
pluginBffVersion: options.pluginBffVersion,
|
|
4720
|
+
pluginI18nVersion: options.pluginI18nVersion,
|
|
4721
|
+
tanstackRouterVersion: options.tanstackRouterVersion,
|
|
4722
|
+
tailwindVersion: options.tailwindVersion,
|
|
4723
|
+
tailwindPostcssVersion: options.tailwindPostcssVersion,
|
|
4724
|
+
pnpmVersion: options.pnpmVersion,
|
|
2296
4725
|
isSubproject: options.isSubproject,
|
|
2297
4726
|
isTanstackRouter: 'tanstack' === options.routerFramework,
|
|
2298
4727
|
enableBff: 'none' !== options.bffRuntime,
|
|
@@ -2300,7 +4729,7 @@ function copyTemplate(src, dest, options) {
|
|
|
2300
4729
|
useHonoBff: 'hono' === options.bffRuntime,
|
|
2301
4730
|
bffRuntime: options.bffRuntime,
|
|
2302
4731
|
enableTailwind: options.enableTailwind,
|
|
2303
|
-
|
|
4732
|
+
routerRuntimeImport: 'tanstack' === options.routerFramework ? '@modern-js/plugin-tanstack/runtime' : '@modern-js/runtime/router'
|
|
2304
4733
|
});
|
|
2305
4734
|
if (0 === rendered.trim().length) continue;
|
|
2306
4735
|
destPath = destPath.replace(/\.handlebars$/, '');
|