@bleedingdev/modern-js-create 3.2.0-ultramodern.5 → 3.2.0-ultramodern.51
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 +118 -15
- package/dist/index.js +5125 -454
- 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 +115 -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 +103 -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 +19 -8
- package/template-workspace/scripts/assert-mf-types.mjs +44 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +163 -0
- package/template-workspace/scripts/setup-agent-reference-repos.mjs +364 -0
- package/template-workspace/scripts/validate-ultramodern-workspace.mjs.handlebars +743 -87
- package/template/biome.json +0 -41
- package/template/src/routes/page.tsx.handlebars +0 -119
|
@@ -1,76 +1,219 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
const root = process.cwd();
|
|
5
6
|
const packageScope = '{{packageScope}}';
|
|
6
|
-
const tanstackVersion = '1.170.
|
|
7
|
+
const tanstackVersion = '1.170.8';
|
|
8
|
+
const tailwindEnabled = '{{tailwindEnabled}}' === 'true';
|
|
9
|
+
const tailwindVersion = '^4.3.0';
|
|
10
|
+
const tailwindPostcssVersion = '^4.3.0';
|
|
11
|
+
const expectedPnpmVersion = '{{pnpmVersion}}';
|
|
12
|
+
const rstackAgentSkillsCommit = '61c948b42512e223bad44b83af4080eba48b2677';
|
|
13
|
+
const moduleFederationAgentSkillsCommit = '07bb5b6c43ad457609e00c081b72d4c42508ec76';
|
|
7
14
|
const modernPackages = [
|
|
8
15
|
'@modern-js/app-tools',
|
|
9
16
|
'@modern-js/plugin-bff',
|
|
17
|
+
'@modern-js/plugin-i18n',
|
|
10
18
|
'@modern-js/plugin-tanstack',
|
|
11
19
|
'@modern-js/runtime',
|
|
12
20
|
];
|
|
21
|
+
const baselineAgentSkills = [
|
|
22
|
+
'rsbuild-best-practices',
|
|
23
|
+
'rspack-best-practices',
|
|
24
|
+
'rspack-tracing',
|
|
25
|
+
'rsdoctor-analysis',
|
|
26
|
+
'rslib-best-practices',
|
|
27
|
+
'rslib-modern-package',
|
|
28
|
+
'rstest-best-practices',
|
|
29
|
+
];
|
|
30
|
+
const moduleFederationAgentSkills = ['mf'];
|
|
31
|
+
const privateAgentSkills = ['plan-graph', 'dag', 'subagent-graph', 'helm', 'debugger-mode'];
|
|
32
|
+
const fullStackVerticals = [
|
|
33
|
+
{
|
|
34
|
+
id: 'remote-commerce',
|
|
35
|
+
domain: 'commerce',
|
|
36
|
+
stem: 'recommendations',
|
|
37
|
+
group: 'recommendations',
|
|
38
|
+
path: 'apps/remotes/remote-commerce',
|
|
39
|
+
mfName: 'remoteCommerce',
|
|
40
|
+
apiPrefix: '/commerce-api',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: 'remote-identity',
|
|
44
|
+
domain: 'identity',
|
|
45
|
+
stem: 'identity',
|
|
46
|
+
group: 'identity',
|
|
47
|
+
path: 'apps/remotes/remote-identity',
|
|
48
|
+
mfName: 'remoteIdentity',
|
|
49
|
+
apiPrefix: '/identity-api',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const designSystemRemotePath = 'apps/remotes/remote-design-system';
|
|
13
53
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
54
|
+
const readText = (relativePath) => fs.readFileSync(path.join(root, relativePath), 'utf-8');
|
|
55
|
+
const readJson = (relativePath) => JSON.parse(readText(relativePath));
|
|
56
|
+
const activePnpmVersion = execFileSync('pnpm', ['--version'], {
|
|
57
|
+
cwd: root,
|
|
58
|
+
encoding: 'utf-8',
|
|
59
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
60
|
+
}).trim();
|
|
61
|
+
const readPnpmConfig = (key) => {
|
|
62
|
+
const env = { ...process.env };
|
|
63
|
+
for (const envKey of Object.keys(env)) {
|
|
64
|
+
if (/^(?:npm|pnpm)_config_/i.test(envKey)) {
|
|
65
|
+
delete env[envKey];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const output = execFileSync('pnpm', ['config', 'get', key, '--json'], {
|
|
69
|
+
cwd: root,
|
|
70
|
+
env,
|
|
71
|
+
encoding: 'utf-8',
|
|
72
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
73
|
+
}).trim();
|
|
74
|
+
return output ? JSON.parse(output) : undefined;
|
|
75
|
+
};
|
|
76
|
+
const assert = (condition, message) => {
|
|
23
77
|
if (!condition) {
|
|
24
78
|
throw new Error(message);
|
|
25
79
|
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function assertExists(relativePath) {
|
|
80
|
+
};
|
|
81
|
+
const assertExists = (relativePath) => {
|
|
29
82
|
assert(fs.existsSync(path.join(root, relativePath)), `Missing ${relativePath}`);
|
|
30
|
-
}
|
|
83
|
+
};
|
|
84
|
+
const assertNotExists = (relativePath) => {
|
|
85
|
+
assert(!fs.existsSync(path.join(root, relativePath)), `Unexpected ${relativePath}`);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
assert(
|
|
89
|
+
activePnpmVersion === expectedPnpmVersion,
|
|
90
|
+
`Generated workspace requires pnpm ${expectedPnpmVersion}; active pnpm is ${activePnpmVersion}. Run mise install, then rerun through mise exec -- pnpm ...`,
|
|
91
|
+
);
|
|
31
92
|
|
|
32
93
|
const requiredPaths = [
|
|
94
|
+
'AGENTS.md',
|
|
95
|
+
'.gitignore',
|
|
33
96
|
'package.json',
|
|
34
97
|
'pnpm-workspace.yaml',
|
|
35
98
|
'tsconfig.base.json',
|
|
99
|
+
'oxlint.config.ts',
|
|
100
|
+
'oxfmt.config.ts',
|
|
101
|
+
'.github/renovate.json',
|
|
102
|
+
'.github/workflows/ultramodern-workspace-gates.yml',
|
|
103
|
+
'.agents/skills-lock.json',
|
|
104
|
+
'.agents/agent-reference-repos.json',
|
|
105
|
+
'.agents/rstackjs-agent-skills-LICENSE',
|
|
106
|
+
'.agents/skills/rsbuild-best-practices/SKILL.md',
|
|
107
|
+
'.agents/skills/rspack-best-practices/SKILL.md',
|
|
108
|
+
'.agents/skills/rspack-tracing/SKILL.md',
|
|
109
|
+
'.agents/skills/rspack-tracing/references/tracing-guide.md',
|
|
110
|
+
'.agents/skills/rspack-tracing/scripts/analyze_trace.js',
|
|
111
|
+
'.agents/skills/rsdoctor-analysis/SKILL.md',
|
|
112
|
+
'.agents/skills/rsdoctor-analysis/references/rsdoctor-data-types.md',
|
|
113
|
+
'.agents/skills/rslib-best-practices/SKILL.md',
|
|
114
|
+
'.agents/skills/rslib-modern-package/SKILL.md',
|
|
115
|
+
'.agents/skills/rstest-best-practices/SKILL.md',
|
|
36
116
|
'topology/reference-topology.json',
|
|
37
117
|
'topology/ownership.json',
|
|
38
118
|
'topology/local-overlays/development.json',
|
|
39
119
|
'.modernjs/ultramodern-workspace-template-manifest.json',
|
|
40
120
|
'.modernjs/ultramodern-package-source.json',
|
|
121
|
+
'.modernjs/ultramodern-generated-contract.json',
|
|
122
|
+
'scripts/assert-mf-types.mjs',
|
|
123
|
+
'scripts/bootstrap-agent-skills.mjs',
|
|
124
|
+
'scripts/setup-agent-reference-repos.mjs',
|
|
41
125
|
'apps/shell-super-app/package.json',
|
|
42
126
|
'apps/shell-super-app/modern.config.ts',
|
|
43
127
|
'apps/shell-super-app/module-federation.config.ts',
|
|
128
|
+
'apps/shell-super-app/src/modern-app-env.d.ts',
|
|
129
|
+
'apps/shell-super-app/src/modern.runtime.ts',
|
|
130
|
+
'apps/shell-super-app/locales/en/translation.json',
|
|
131
|
+
'apps/shell-super-app/locales/cs/translation.json',
|
|
132
|
+
'apps/shell-super-app/src/routes/index.css',
|
|
133
|
+
'apps/shell-super-app/src/routes/layout.tsx',
|
|
134
|
+
'apps/shell-super-app/src/routes/[lang]/page.tsx',
|
|
44
135
|
'apps/remotes/remote-commerce/package.json',
|
|
45
136
|
'apps/remotes/remote-commerce/modern.config.ts',
|
|
46
137
|
'apps/remotes/remote-commerce/module-federation.config.ts',
|
|
138
|
+
'apps/remotes/remote-commerce/api/effect/index.ts',
|
|
139
|
+
'apps/remotes/remote-commerce/shared/effect/api.ts',
|
|
140
|
+
'apps/remotes/remote-commerce/src/effect/recommendations-client.ts',
|
|
141
|
+
'apps/remotes/remote-commerce/src/modern-app-env.d.ts',
|
|
142
|
+
'apps/remotes/remote-commerce/src/modern.runtime.ts',
|
|
143
|
+
'apps/remotes/remote-commerce/locales/en/translation.json',
|
|
144
|
+
'apps/remotes/remote-commerce/locales/cs/translation.json',
|
|
145
|
+
'apps/remotes/remote-commerce/src/routes/index.css',
|
|
146
|
+
'apps/remotes/remote-commerce/src/routes/layout.tsx',
|
|
147
|
+
'apps/remotes/remote-commerce/src/routes/[lang]/page.tsx',
|
|
47
148
|
'apps/remotes/remote-identity/package.json',
|
|
48
149
|
'apps/remotes/remote-identity/modern.config.ts',
|
|
49
150
|
'apps/remotes/remote-identity/module-federation.config.ts',
|
|
151
|
+
'apps/remotes/remote-identity/api/effect/index.ts',
|
|
152
|
+
'apps/remotes/remote-identity/shared/effect/api.ts',
|
|
153
|
+
'apps/remotes/remote-identity/src/effect/identity-client.ts',
|
|
154
|
+
'apps/remotes/remote-identity/src/modern-app-env.d.ts',
|
|
155
|
+
'apps/remotes/remote-identity/src/modern.runtime.ts',
|
|
156
|
+
'apps/remotes/remote-identity/locales/en/translation.json',
|
|
157
|
+
'apps/remotes/remote-identity/locales/cs/translation.json',
|
|
158
|
+
'apps/remotes/remote-identity/src/routes/index.css',
|
|
159
|
+
'apps/remotes/remote-identity/src/routes/layout.tsx',
|
|
160
|
+
'apps/remotes/remote-identity/src/routes/[lang]/page.tsx',
|
|
50
161
|
'apps/remotes/remote-design-system/package.json',
|
|
51
162
|
'apps/remotes/remote-design-system/modern.config.ts',
|
|
52
163
|
'apps/remotes/remote-design-system/module-federation.config.ts',
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
'
|
|
164
|
+
'apps/remotes/remote-design-system/src/modern-app-env.d.ts',
|
|
165
|
+
'apps/remotes/remote-design-system/src/modern.runtime.ts',
|
|
166
|
+
'apps/remotes/remote-design-system/locales/en/translation.json',
|
|
167
|
+
'apps/remotes/remote-design-system/locales/cs/translation.json',
|
|
168
|
+
'apps/remotes/remote-design-system/src/routes/index.css',
|
|
169
|
+
'apps/remotes/remote-design-system/src/routes/layout.tsx',
|
|
170
|
+
'apps/remotes/remote-design-system/src/routes/[lang]/page.tsx',
|
|
57
171
|
'packages/shared-contracts/src/index.ts',
|
|
58
172
|
'packages/shared-design-tokens/src/index.ts',
|
|
59
173
|
'packages/shared-effect-api/src/index.ts',
|
|
60
174
|
];
|
|
61
175
|
|
|
176
|
+
if (tailwindEnabled) {
|
|
177
|
+
requiredPaths.push(
|
|
178
|
+
'apps/shell-super-app/postcss.config.mjs',
|
|
179
|
+
'apps/shell-super-app/tailwind.config.ts',
|
|
180
|
+
'apps/remotes/remote-commerce/postcss.config.mjs',
|
|
181
|
+
'apps/remotes/remote-commerce/tailwind.config.ts',
|
|
182
|
+
'apps/remotes/remote-identity/postcss.config.mjs',
|
|
183
|
+
'apps/remotes/remote-identity/tailwind.config.ts',
|
|
184
|
+
'apps/remotes/remote-design-system/postcss.config.mjs',
|
|
185
|
+
'apps/remotes/remote-design-system/tailwind.config.ts',
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
62
189
|
for (const requiredPath of requiredPaths) {
|
|
63
190
|
assertExists(requiredPath);
|
|
64
191
|
}
|
|
65
192
|
|
|
193
|
+
assertNotExists('services/service-recommendations-effect');
|
|
194
|
+
|
|
195
|
+
for (const appDirectory of [
|
|
196
|
+
'apps/shell-super-app',
|
|
197
|
+
'apps/remotes/remote-commerce',
|
|
198
|
+
'apps/remotes/remote-identity',
|
|
199
|
+
'apps/remotes/remote-design-system',
|
|
200
|
+
]) {
|
|
201
|
+
assert(
|
|
202
|
+
!fs.existsSync(path.join(root, appDirectory, 'src/routes/page.tsx')),
|
|
203
|
+
`${appDirectory} must use src/routes/[lang]/page.tsx`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
66
207
|
const rootPackage = readJson('package.json');
|
|
67
208
|
const packageSource = readJson('.modernjs/ultramodern-package-source.json');
|
|
209
|
+
const skillsLock = readJson('.agents/skills-lock.json');
|
|
210
|
+
const agentReferenceRepos = readJson('.agents/agent-reference-repos.json');
|
|
211
|
+
const workflowContent = readText('.github/workflows/ultramodern-workspace-gates.yml');
|
|
212
|
+
const renovateConfig = readJson('.github/renovate.json');
|
|
68
213
|
const expectedModernSpecifier =
|
|
69
|
-
packageSource.strategy === 'install'
|
|
70
|
-
? packageSource.modernPackages?.specifier
|
|
71
|
-
: 'workspace:*';
|
|
214
|
+
packageSource.strategy === 'install' ? packageSource.modernPackages?.specifier : 'workspace:*';
|
|
72
215
|
|
|
73
|
-
|
|
216
|
+
const expectedModernDependency = (packageName) => {
|
|
74
217
|
if (packageSource.strategy !== 'install') {
|
|
75
218
|
return 'workspace:*';
|
|
76
219
|
}
|
|
@@ -79,13 +222,109 @@ function expectedModernDependency(packageName) {
|
|
|
79
222
|
return aliasPackageName
|
|
80
223
|
? `npm:${aliasPackageName}@${expectedModernSpecifier}`
|
|
81
224
|
: expectedModernSpecifier;
|
|
82
|
-
}
|
|
225
|
+
};
|
|
83
226
|
|
|
84
227
|
assert(rootPackage.private === true, 'Root package must be private');
|
|
85
228
|
assert(rootPackage.modernjs?.preset === 'presetUltramodern', 'Root must declare presetUltramodern');
|
|
229
|
+
const miseConfig = readText('.mise.toml');
|
|
230
|
+
assert(
|
|
231
|
+
miseConfig.includes(`pnpm = "${expectedPnpmVersion}"`),
|
|
232
|
+
`Root must pin pnpm ${expectedPnpmVersion} in .mise.toml`,
|
|
233
|
+
);
|
|
234
|
+
assert(
|
|
235
|
+
rootPackage.packageManager === `pnpm@${expectedPnpmVersion}`,
|
|
236
|
+
`Root must pin pnpm ${expectedPnpmVersion}`,
|
|
237
|
+
);
|
|
238
|
+
assert(
|
|
239
|
+
rootPackage.engines?.pnpm === `>=${expectedPnpmVersion} <11.5.0`,
|
|
240
|
+
`Root must require pnpm >=${expectedPnpmVersion} <11.5.0`,
|
|
241
|
+
);
|
|
242
|
+
assert(
|
|
243
|
+
JSON.stringify(readPnpmConfig('packages')) ===
|
|
244
|
+
JSON.stringify(['apps/*', 'apps/remotes/*', 'services/*', 'packages/*']),
|
|
245
|
+
'pnpm-workspace.yaml must retain workspace package globs',
|
|
246
|
+
);
|
|
247
|
+
assert(readPnpmConfig('minimumReleaseAge') === 0, 'pnpm minimumReleaseAge must be 0');
|
|
248
|
+
assert(readPnpmConfig('minimumReleaseAgeStrict') === undefined, 'pnpm minimumReleaseAgeStrict must stay unset');
|
|
249
|
+
assert(
|
|
250
|
+
readPnpmConfig('minimumReleaseAgeIgnoreMissingTime') === undefined,
|
|
251
|
+
'pnpm minimumReleaseAgeIgnoreMissingTime must stay unset',
|
|
252
|
+
);
|
|
253
|
+
assert(
|
|
254
|
+
readPnpmConfig('minimumReleaseAgeExclude') === undefined,
|
|
255
|
+
'pnpm minimumReleaseAgeExclude must stay unset',
|
|
256
|
+
);
|
|
257
|
+
assert(readPnpmConfig('trustPolicy') === undefined, 'pnpm trustPolicy must stay unset');
|
|
258
|
+
assert(readPnpmConfig('trustPolicyIgnoreAfter') === undefined, 'pnpm trustPolicyIgnoreAfter must stay unset');
|
|
259
|
+
assert(readPnpmConfig('blockExoticSubdeps') === true, 'pnpm blockExoticSubdeps must be true');
|
|
260
|
+
assert(readPnpmConfig('engineStrict') === true, 'pnpm engineStrict must be true');
|
|
261
|
+
assert(readPnpmConfig('pmOnFail') === 'error', 'pnpm pmOnFail must be error');
|
|
262
|
+
assert(readPnpmConfig('verifyDepsBeforeRun') === 'error', 'pnpm verifyDepsBeforeRun must be error');
|
|
263
|
+
assert(readPnpmConfig('strictDepBuilds') === true, 'pnpm strictDepBuilds must be true');
|
|
264
|
+
assert(
|
|
265
|
+
JSON.stringify(readPnpmConfig('peerDependencyRules')) ===
|
|
266
|
+
JSON.stringify({ allowedVersions: { react: '>=19.0.0', typescript: '>=6.0.0' } }),
|
|
267
|
+
'pnpm peerDependencyRules must allow React 19 and TypeScript 6+',
|
|
268
|
+
);
|
|
269
|
+
assert(
|
|
270
|
+
JSON.stringify(readPnpmConfig('overrides')) ===
|
|
271
|
+
JSON.stringify({ '@tanstack/react-router': '1.170.8', 'node-fetch': '^3.3.2' }),
|
|
272
|
+
'pnpm overrides must pin generated router and node-fetch policy',
|
|
273
|
+
);
|
|
274
|
+
assert(
|
|
275
|
+
JSON.stringify(readPnpmConfig('allowBuilds')) ===
|
|
276
|
+
JSON.stringify({
|
|
277
|
+
'@swc/core': true,
|
|
278
|
+
'core-js': true,
|
|
279
|
+
esbuild: true,
|
|
280
|
+
'msgpackr-extract': true,
|
|
281
|
+
sharp: true,
|
|
282
|
+
'simple-git-hooks': true,
|
|
283
|
+
workerd: true,
|
|
284
|
+
}),
|
|
285
|
+
'pnpm allowBuilds must approve only the generated workspace build dependencies',
|
|
286
|
+
);
|
|
287
|
+
assert(readPnpmConfig('onlyBuiltDependencies') === undefined, 'pnpm onlyBuiltDependencies must not be set');
|
|
288
|
+
for (const requiredSnippet of [
|
|
289
|
+
'permissions:\n contents: read',
|
|
290
|
+
'pull_request:',
|
|
291
|
+
'persist-credentials: false',
|
|
292
|
+
'jdx/mise-action',
|
|
293
|
+
'mise exec -- pnpm install --frozen-lockfile',
|
|
294
|
+
'mise exec -- pnpm run ultramodern:check',
|
|
295
|
+
'mise exec -- pnpm build',
|
|
296
|
+
'MODERN_PUBLIC_SITE_URL: http://localhost:8080',
|
|
297
|
+
'timeout-minutes:',
|
|
298
|
+
'egress-policy: audit',
|
|
299
|
+
]) {
|
|
300
|
+
assert(
|
|
301
|
+
workflowContent.includes(requiredSnippet),
|
|
302
|
+
`Generated workspace workflow must retain ${requiredSnippet.split('\n')[0]}`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
assert(
|
|
306
|
+
!workflowContent.includes('pull_request_target'),
|
|
307
|
+
'Generated workspace workflow must not use pull_request_target',
|
|
308
|
+
);
|
|
309
|
+
for (const match of workflowContent.matchAll(/^\s*uses:\s*([^@\s]+)@([^\s#]+)/gmu)) {
|
|
310
|
+
const [, actionName, actionRef] = match;
|
|
311
|
+
assert(
|
|
312
|
+
/^[a-f0-9]{40}$/u.test(actionRef),
|
|
313
|
+
`Generated workspace workflow must pin ${actionName}@${actionRef} to a commit SHA`,
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
assert(
|
|
317
|
+
renovateConfig.dependencyDashboard === true &&
|
|
318
|
+
renovateConfig.minimumReleaseAge === '1 day' &&
|
|
319
|
+
renovateConfig.extends?.includes('helpers:pinGitHubActionDigests') &&
|
|
320
|
+
renovateConfig.packageRules?.some(
|
|
321
|
+
(rule) =>
|
|
322
|
+
rule.dependencyDashboardApproval === true && rule.matchUpdateTypes?.includes('major'),
|
|
323
|
+
),
|
|
324
|
+
'Generated workspace Renovate config must retain dashboard, release-age, action pinning, and major-approval policy',
|
|
325
|
+
);
|
|
86
326
|
assert(
|
|
87
|
-
rootPackage.modernjs?.packageSource?.config ===
|
|
88
|
-
'./.modernjs/ultramodern-package-source.json',
|
|
327
|
+
rootPackage.modernjs?.packageSource?.config === './.modernjs/ultramodern-package-source.json',
|
|
89
328
|
'Root must point to the UltraModern package source metadata',
|
|
90
329
|
);
|
|
91
330
|
assert(
|
|
@@ -101,70 +340,361 @@ assert(
|
|
|
101
340
|
'Generated shared packages must keep workspace:* links',
|
|
102
341
|
);
|
|
103
342
|
assert(
|
|
104
|
-
modernPackages.every(packageName =>
|
|
343
|
+
modernPackages.every((packageName) =>
|
|
105
344
|
packageSource.modernPackages?.packages?.includes(packageName),
|
|
106
345
|
),
|
|
107
346
|
'Package source metadata must list all Modern runtime/tooling packages',
|
|
108
347
|
);
|
|
109
348
|
assert(
|
|
110
|
-
expectedModernSpecifier &&
|
|
111
|
-
packageSource.modernPackages?.specifier === expectedModernSpecifier,
|
|
349
|
+
expectedModernSpecifier && packageSource.modernPackages?.specifier === expectedModernSpecifier,
|
|
112
350
|
'Package source metadata must provide a Modern package specifier',
|
|
113
351
|
);
|
|
352
|
+
|
|
353
|
+
const requiredRootScripts = {
|
|
354
|
+
build:
|
|
355
|
+
'pnpm -r --filter "./apps/remotes/**" run build && pnpm --filter "./apps/shell-super-app" run build && pnpm ultramodern:assert-mf-types',
|
|
356
|
+
format: 'oxfmt .',
|
|
357
|
+
'format:check': 'oxfmt --check .',
|
|
358
|
+
lint: 'oxlint .',
|
|
359
|
+
'lint:fix': 'oxlint . --fix',
|
|
360
|
+
'agents:refs:check': 'node ./scripts/setup-agent-reference-repos.mjs --check',
|
|
361
|
+
'agents:refs:install': 'node ./scripts/setup-agent-reference-repos.mjs',
|
|
362
|
+
postinstall:
|
|
363
|
+
'node ./scripts/setup-agent-reference-repos.mjs && node ./scripts/bootstrap-agent-skills.mjs',
|
|
364
|
+
'skills:check': 'node ./scripts/bootstrap-agent-skills.mjs --check',
|
|
365
|
+
'skills:install': 'node ./scripts/bootstrap-agent-skills.mjs',
|
|
366
|
+
typecheck: `pnpm -r --filter "@${packageScope}/*" typecheck`,
|
|
367
|
+
'ultramodern:assert-mf-types': 'node ./scripts/assert-mf-types.mjs',
|
|
368
|
+
'ultramodern:check': 'node ./scripts/validate-ultramodern-workspace.mjs',
|
|
369
|
+
};
|
|
370
|
+
for (const [scriptName, scriptCommand] of Object.entries(requiredRootScripts)) {
|
|
371
|
+
assert(rootPackage.scripts?.[scriptName] === scriptCommand, `Root must expose ${scriptName}`);
|
|
372
|
+
}
|
|
373
|
+
assert(
|
|
374
|
+
Object.keys(rootPackage.scripts ?? {}).every((scriptName) => !scriptName.startsWith('zephyr:')),
|
|
375
|
+
'Root package must not expose custom zephyr:* lifecycle commands',
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
for (const dependency of [
|
|
379
|
+
'@effect/tsgo',
|
|
380
|
+
'@typescript/native-preview',
|
|
381
|
+
'oxfmt',
|
|
382
|
+
'oxlint',
|
|
383
|
+
'ultracite',
|
|
384
|
+
]) {
|
|
385
|
+
assert(rootPackage.devDependencies?.[dependency], `Root must depend on ${dependency}`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const agentsInstructions = readText('AGENTS.md');
|
|
389
|
+
assert(
|
|
390
|
+
agentsInstructions.includes('UltraModern Agent Contract') &&
|
|
391
|
+
agentsInstructions.includes('Required Skill Baseline'),
|
|
392
|
+
'Root AGENTS.md must document the UltraModern agent contract',
|
|
393
|
+
);
|
|
394
|
+
assert(
|
|
395
|
+
skillsLock.source?.repository === 'https://github.com/rstackjs/agent-skills',
|
|
396
|
+
'Agent skills lock must retain the Rstack skill source repository',
|
|
397
|
+
);
|
|
398
|
+
assert(
|
|
399
|
+
skillsLock.source?.commit === rstackAgentSkillsCommit,
|
|
400
|
+
'Agent skills lock must pin the expected Rstack skill commit',
|
|
401
|
+
);
|
|
402
|
+
assert(
|
|
403
|
+
skillsLock.installDir === '.agents/skills',
|
|
404
|
+
'Agent skills lock must use .agents/skills as installDir',
|
|
405
|
+
);
|
|
406
|
+
assert(
|
|
407
|
+
agentReferenceRepos.defaultEnabled === true,
|
|
408
|
+
'Agent reference repositories must be enabled by default',
|
|
409
|
+
);
|
|
410
|
+
assert(
|
|
411
|
+
agentReferenceRepos.strategy === 'git-subtree-squash',
|
|
412
|
+
'Agent reference repositories must use git subtree squash strategy',
|
|
413
|
+
);
|
|
414
|
+
assert(
|
|
415
|
+
agentReferenceRepos.repositories?.some(
|
|
416
|
+
(repo) =>
|
|
417
|
+
repo.id === 'effect' &&
|
|
418
|
+
repo.url === 'https://github.com/Effect-TS/effect.git' &&
|
|
419
|
+
repo.path === 'repos/effect',
|
|
420
|
+
),
|
|
421
|
+
'Agent reference repositories must include Effect',
|
|
422
|
+
);
|
|
423
|
+
assert(
|
|
424
|
+
agentReferenceRepos.repositories?.some(
|
|
425
|
+
(repo) =>
|
|
426
|
+
repo.id === 'ultramodern-js' &&
|
|
427
|
+
repo.url === 'https://github.com/BleedingDev/ultramodern.js.git' &&
|
|
428
|
+
repo.path === 'repos/ultramodern.js',
|
|
429
|
+
),
|
|
430
|
+
'Agent reference repositories must include UltraModern.js',
|
|
431
|
+
);
|
|
114
432
|
assert(
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
|
|
433
|
+
readText('scripts/setup-agent-reference-repos.mjs').includes(
|
|
434
|
+
'ULTRAMODERN_SKIP_AGENT_REPOS',
|
|
435
|
+
),
|
|
436
|
+
'Agent reference repository setup must expose an opt-out environment variable',
|
|
437
|
+
);
|
|
438
|
+
assert(
|
|
439
|
+
readText('scripts/setup-agent-reference-repos.mjs').includes('git-subtree-dir'),
|
|
440
|
+
'Agent reference repository setup must validate git subtree evidence',
|
|
441
|
+
);
|
|
442
|
+
for (const skillName of baselineAgentSkills) {
|
|
443
|
+
assert(
|
|
444
|
+
skillsLock.baseline?.some((skill) => skill.name === skillName),
|
|
445
|
+
`Agent skills lock must include ${skillName}`,
|
|
446
|
+
);
|
|
447
|
+
assert(
|
|
448
|
+
readText(`.agents/skills/${skillName}/SKILL.md`).includes(`name: ${skillName}`),
|
|
449
|
+
`${skillName} must contain matching skill metadata`,
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const privateSource = skillsLock.sources?.find(
|
|
454
|
+
(source) => source.repository === 'https://github.com/TechsioCZ/skills',
|
|
455
|
+
);
|
|
456
|
+
const moduleFederationSource = skillsLock.sources?.find(
|
|
457
|
+
(source) => source.repository === 'https://github.com/module-federation/agent-skills',
|
|
458
|
+
);
|
|
459
|
+
assert(
|
|
460
|
+
moduleFederationSource?.install === 'clone' &&
|
|
461
|
+
moduleFederationSource.commit === moduleFederationAgentSkillsCommit,
|
|
462
|
+
'Agent skills lock must configure the public Module Federation skill source as a pinned clone',
|
|
118
463
|
);
|
|
464
|
+
for (const skillName of moduleFederationAgentSkills) {
|
|
465
|
+
assert(
|
|
466
|
+
skillsLock.baseline?.some((skill) => skill.name === skillName) &&
|
|
467
|
+
moduleFederationSource.baseline?.some((skill) => skill.name === skillName),
|
|
468
|
+
`Agent skills lock must require Module Federation skill ${skillName}`,
|
|
469
|
+
);
|
|
470
|
+
assert(
|
|
471
|
+
agentsInstructions.includes(`\`${skillName}\``) &&
|
|
472
|
+
agentsInstructions.includes('module-federation/agent-skills'),
|
|
473
|
+
`Root AGENTS.md must document required Module Federation skill ${skillName}`,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
assert(
|
|
477
|
+
privateSource?.install === 'clone-if-authorized',
|
|
478
|
+
'Agent skills lock must configure TechsioCZ skills as clone-if-authorized',
|
|
479
|
+
);
|
|
480
|
+
for (const skillName of privateAgentSkills) {
|
|
481
|
+
assert(
|
|
482
|
+
privateSource.baseline?.some((skill) => skill.name === skillName),
|
|
483
|
+
`Agent skills lock must pin private skill ${skillName}`,
|
|
484
|
+
);
|
|
485
|
+
}
|
|
119
486
|
|
|
120
487
|
const appPackagePaths = [
|
|
121
488
|
'apps/shell-super-app/package.json',
|
|
122
489
|
'apps/remotes/remote-commerce/package.json',
|
|
123
490
|
'apps/remotes/remote-identity/package.json',
|
|
124
|
-
|
|
491
|
+
`${designSystemRemotePath}/package.json`,
|
|
125
492
|
];
|
|
126
493
|
|
|
494
|
+
const expectedZephyrDependencies = {
|
|
495
|
+
commerce: `@${packageScope}/remote-commerce@workspace:*`,
|
|
496
|
+
identity: `@${packageScope}/remote-identity@workspace:*`,
|
|
497
|
+
designSystem: `@${packageScope}/remote-design-system@workspace:*`,
|
|
498
|
+
};
|
|
499
|
+
|
|
127
500
|
for (const packagePath of appPackagePaths) {
|
|
128
501
|
const packageJson = readJson(packagePath);
|
|
502
|
+
const isRemote = packagePath.includes('/remotes/');
|
|
503
|
+
const fullStackVertical = fullStackVerticals.find(
|
|
504
|
+
(vertical) => `${vertical.path}/package.json` === packagePath,
|
|
505
|
+
);
|
|
506
|
+
assert(packageJson.scripts?.dev === 'modern dev', `${packagePath} must use vanilla modern dev`);
|
|
507
|
+
assert(
|
|
508
|
+
packageJson.scripts?.build ===
|
|
509
|
+
(isRemote
|
|
510
|
+
? 'modern build && node ../../../scripts/assert-mf-types.mjs'
|
|
511
|
+
: 'modern build'),
|
|
512
|
+
`${packagePath} must enforce Module Federation DTS on remote builds`,
|
|
513
|
+
);
|
|
514
|
+
assert(packageJson.scripts?.serve === 'modern serve', `${packagePath} must use vanilla modern serve`);
|
|
515
|
+
assert(
|
|
516
|
+
Object.keys(packageJson.scripts ?? {}).every((scriptName) => !scriptName.startsWith('zephyr:')),
|
|
517
|
+
`${packagePath} must not expose custom zephyr:* lifecycle commands`,
|
|
518
|
+
);
|
|
519
|
+
assert(
|
|
520
|
+
packageJson['zephyr:dependencies'] &&
|
|
521
|
+
typeof packageJson['zephyr:dependencies'] === 'object' &&
|
|
522
|
+
!Array.isArray(packageJson['zephyr:dependencies']),
|
|
523
|
+
`${packagePath} must declare zephyr:dependencies for deterministic Zephyr dependency extraction`,
|
|
524
|
+
);
|
|
525
|
+
if (isRemote) {
|
|
526
|
+
assert(
|
|
527
|
+
Object.keys(packageJson['zephyr:dependencies']).length === 0,
|
|
528
|
+
`${packagePath} must declare an empty zephyr:dependencies map until it consumes remotes`,
|
|
529
|
+
);
|
|
530
|
+
} else {
|
|
531
|
+
assert(
|
|
532
|
+
JSON.stringify(packageJson['zephyr:dependencies']) ===
|
|
533
|
+
JSON.stringify(expectedZephyrDependencies),
|
|
534
|
+
`${packagePath} must map shell remote aliases to Zephyr workspace remote package dependencies`,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
129
537
|
assert(
|
|
130
538
|
packageJson.dependencies?.['@modern-js/plugin-tanstack'] ===
|
|
131
539
|
expectedModernDependency('@modern-js/plugin-tanstack'),
|
|
132
|
-
`${packagePath} must
|
|
540
|
+
`${packagePath} must use @modern-js/plugin-tanstack through ${expectedModernDependency(
|
|
541
|
+
'@modern-js/plugin-tanstack',
|
|
542
|
+
)}`,
|
|
543
|
+
);
|
|
544
|
+
assert(
|
|
545
|
+
packageJson.dependencies?.['@modern-js/plugin-i18n'] ===
|
|
546
|
+
expectedModernDependency('@modern-js/plugin-i18n'),
|
|
547
|
+
`${packagePath} must use @modern-js/plugin-i18n through ${expectedModernDependency(
|
|
548
|
+
'@modern-js/plugin-i18n',
|
|
549
|
+
)}`,
|
|
133
550
|
);
|
|
134
551
|
assert(
|
|
135
552
|
packageJson.dependencies?.['@modern-js/runtime'] ===
|
|
136
553
|
expectedModernDependency('@modern-js/runtime'),
|
|
137
|
-
`${packagePath} must
|
|
554
|
+
`${packagePath} must use @modern-js/runtime through ${expectedModernDependency(
|
|
555
|
+
'@modern-js/runtime',
|
|
556
|
+
)}`,
|
|
138
557
|
);
|
|
139
558
|
assert(
|
|
140
559
|
packageJson.devDependencies?.['@modern-js/app-tools'] ===
|
|
141
560
|
expectedModernDependency('@modern-js/app-tools'),
|
|
142
|
-
`${packagePath} must
|
|
561
|
+
`${packagePath} must use @modern-js/app-tools through ${expectedModernDependency(
|
|
562
|
+
'@modern-js/app-tools',
|
|
563
|
+
)}`,
|
|
564
|
+
);
|
|
565
|
+
assert(
|
|
566
|
+
packageJson.devDependencies?.typescript === '6.0.3',
|
|
567
|
+
`${packagePath} must include TypeScript 6 only as the stable JS TypeScript package for Module Federation DTS generation`,
|
|
568
|
+
);
|
|
569
|
+
assert(
|
|
570
|
+
packageJson.devDependencies?.['@typescript/native-preview'] === '7.0.0-dev.20260527.2',
|
|
571
|
+
`${packagePath} must include TypeScript 7 native preview for mandatory native typechecking`,
|
|
143
572
|
);
|
|
144
573
|
assert(
|
|
145
|
-
packageJson.
|
|
146
|
-
|
|
574
|
+
packageJson.devDependencies?.['zephyr-rspack-plugin'] === '1.1.1',
|
|
575
|
+
`${packagePath} must install the Zephyr Rspack plugin used by the Modern.js bridge`,
|
|
576
|
+
);
|
|
577
|
+
assert(
|
|
578
|
+
packageJson.devDependencies?.['zephyr-modernjs-plugin'] === undefined,
|
|
579
|
+
`${packagePath} must not install the inactive Zephyr Modern.js wrapper`,
|
|
580
|
+
);
|
|
581
|
+
assert(
|
|
582
|
+
packageJson.dependencies?.[`@${packageScope}/shared-contracts`] === 'workspace:*',
|
|
147
583
|
`${packagePath} must link generated shared contracts through workspace:*`,
|
|
148
584
|
);
|
|
149
585
|
assert(
|
|
150
|
-
packageJson.dependencies?.[`@${packageScope}/shared-design-tokens`] ===
|
|
151
|
-
'workspace:*',
|
|
586
|
+
packageJson.dependencies?.[`@${packageScope}/shared-design-tokens`] === 'workspace:*',
|
|
152
587
|
`${packagePath} must link generated shared design tokens through workspace:*`,
|
|
153
588
|
);
|
|
589
|
+
assert(
|
|
590
|
+
packageJson.dependencies?.['node-fetch'] === '^3.3.2',
|
|
591
|
+
`${packagePath} must satisfy the Module Federation SDK node-fetch peer`,
|
|
592
|
+
);
|
|
154
593
|
assert(
|
|
155
594
|
packageJson.dependencies?.['@tanstack/react-router'] === tanstackVersion,
|
|
156
595
|
`${packagePath} must use @tanstack/react-router ${tanstackVersion}`,
|
|
157
596
|
);
|
|
158
597
|
assert(
|
|
159
|
-
packageJson.dependencies?.
|
|
598
|
+
packageJson.dependencies?.i18next === '26.2.0',
|
|
599
|
+
`${packagePath} must include i18next for mandatory native Modern.js i18n`,
|
|
600
|
+
);
|
|
601
|
+
assert(
|
|
602
|
+
!packageJson.dependencies?.['react-i18next'],
|
|
603
|
+
`${packagePath} must use native @modern-js/plugin-i18n hooks instead of react-i18next in the TanStack MF starter`,
|
|
604
|
+
);
|
|
605
|
+
assert(
|
|
606
|
+
packageJson.dependencies?.['@module-federation/modern-js-v3'] === '2.5.0',
|
|
160
607
|
`${packagePath} must include the Module Federation plugin`,
|
|
161
608
|
);
|
|
609
|
+
if (fullStackVertical) {
|
|
610
|
+
assert(
|
|
611
|
+
packageJson.dependencies?.['@modern-js/plugin-bff'] ===
|
|
612
|
+
expectedModernDependency('@modern-js/plugin-bff'),
|
|
613
|
+
`${packagePath} must use @modern-js/plugin-bff through ${expectedModernDependency(
|
|
614
|
+
'@modern-js/plugin-bff',
|
|
615
|
+
)}`,
|
|
616
|
+
);
|
|
617
|
+
assert(
|
|
618
|
+
packageJson.exports?.['./effect/client'] ===
|
|
619
|
+
`./src/effect/${fullStackVertical.stem}-client.ts` &&
|
|
620
|
+
packageJson.exports?.['./shared/effect/api'] === './shared/effect/api.ts',
|
|
621
|
+
`${packagePath} must expose its vertical-owned Effect client and contract surface`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
if (packagePath === `${designSystemRemotePath}/package.json`) {
|
|
625
|
+
assert(
|
|
626
|
+
!packageJson.dependencies?.['@modern-js/plugin-bff'],
|
|
627
|
+
'Design-system remote must remain FE-only and must not depend on @modern-js/plugin-bff',
|
|
628
|
+
);
|
|
629
|
+
assert(
|
|
630
|
+
!packageJson.exports?.['./effect/client'] && !packageJson.exports?.['./shared/effect/api'],
|
|
631
|
+
'Design-system remote must not expose Effect client or contract exports',
|
|
632
|
+
);
|
|
633
|
+
}
|
|
634
|
+
if (tailwindEnabled) {
|
|
635
|
+
assert(
|
|
636
|
+
packageJson.devDependencies?.tailwindcss === tailwindVersion,
|
|
637
|
+
`${packagePath} must include Tailwind CSS ${tailwindVersion}`,
|
|
638
|
+
);
|
|
639
|
+
assert(
|
|
640
|
+
packageJson.devDependencies?.['@tailwindcss/postcss'] === tailwindPostcssVersion,
|
|
641
|
+
`${packagePath} must include @tailwindcss/postcss ${tailwindPostcssVersion}`,
|
|
642
|
+
);
|
|
643
|
+
assert(
|
|
644
|
+
packageJson.devDependencies?.postcss === '^8.5.6',
|
|
645
|
+
`${packagePath} must include PostCSS for Tailwind CSS`,
|
|
646
|
+
);
|
|
647
|
+
} else {
|
|
648
|
+
assert(!packageJson.devDependencies?.tailwindcss, `${packagePath} must not include Tailwind CSS`);
|
|
649
|
+
assert(
|
|
650
|
+
!packageJson.devDependencies?.['@tailwindcss/postcss'],
|
|
651
|
+
`${packagePath} must not include @tailwindcss/postcss`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
162
654
|
assert(
|
|
163
655
|
packageJson.modernjs?.preset === 'presetUltramodern',
|
|
164
656
|
`${packagePath} must keep presetUltramodern metadata`,
|
|
165
657
|
);
|
|
166
658
|
}
|
|
167
659
|
|
|
660
|
+
for (const federationConfigPath of [
|
|
661
|
+
'apps/shell-super-app/module-federation.config.ts',
|
|
662
|
+
'apps/remotes/remote-commerce/module-federation.config.ts',
|
|
663
|
+
'apps/remotes/remote-identity/module-federation.config.ts',
|
|
664
|
+
'apps/remotes/remote-design-system/module-federation.config.ts',
|
|
665
|
+
]) {
|
|
666
|
+
const federationConfig = readText(federationConfigPath);
|
|
667
|
+
assert(
|
|
668
|
+
federationConfig.includes('compilerInstance:') &&
|
|
669
|
+
federationConfig.includes("'--package typescript -- tsc'") &&
|
|
670
|
+
federationConfig.includes('displayErrorInTerminal: true') &&
|
|
671
|
+
!federationConfig.includes('dts: false'),
|
|
672
|
+
`${federationConfigPath} must keep strict Module Federation DTS enabled`,
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for (const appDirectory of [
|
|
677
|
+
'apps/shell-super-app',
|
|
678
|
+
'apps/remotes/remote-commerce',
|
|
679
|
+
'apps/remotes/remote-identity',
|
|
680
|
+
'apps/remotes/remote-design-system',
|
|
681
|
+
]) {
|
|
682
|
+
if (tailwindEnabled) {
|
|
683
|
+
assertExists(`${appDirectory}/postcss.config.mjs`);
|
|
684
|
+
assertExists(`${appDirectory}/tailwind.config.ts`);
|
|
685
|
+
}
|
|
686
|
+
if (!tailwindEnabled) {
|
|
687
|
+
assert(
|
|
688
|
+
!fs.existsSync(path.join(root, appDirectory, 'postcss.config.mjs')),
|
|
689
|
+
`${appDirectory} must not write PostCSS config when Tailwind is disabled`,
|
|
690
|
+
);
|
|
691
|
+
assert(
|
|
692
|
+
!fs.existsSync(path.join(root, appDirectory, 'tailwind.config.ts')),
|
|
693
|
+
`${appDirectory} must not write Tailwind config when Tailwind is disabled`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
168
698
|
for (const configPath of [
|
|
169
699
|
'apps/shell-super-app/modern.config.ts',
|
|
170
700
|
'apps/remotes/remote-commerce/modern.config.ts',
|
|
@@ -173,90 +703,177 @@ for (const configPath of [
|
|
|
173
703
|
]) {
|
|
174
704
|
const config = readText(configPath);
|
|
175
705
|
assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
|
|
706
|
+
assert(config.includes('ULTRAMODERN_SITE_URL'), `${configPath} must expose site URL metadata`);
|
|
707
|
+
assert(
|
|
708
|
+
config.includes('MODERN_PUBLIC_SITE_URL must be set for production builds'),
|
|
709
|
+
`${configPath} must require MODERN_PUBLIC_SITE_URL for production builds`,
|
|
710
|
+
);
|
|
176
711
|
assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
|
|
177
|
-
assert(config.includes('
|
|
712
|
+
assert(config.includes('i18nPlugin('), `${configPath} must enable native Modern.js i18n`);
|
|
713
|
+
assert(
|
|
714
|
+
config.includes('reactI18next: false'),
|
|
715
|
+
`${configPath} must use the native Modern.js i18n provider without optional react-i18next probing`,
|
|
716
|
+
);
|
|
717
|
+
assert(
|
|
718
|
+
config.includes("publicDir: './locales'"),
|
|
719
|
+
`${configPath} must expose app-owned locale JSON through Modern.js publicDir`,
|
|
720
|
+
);
|
|
721
|
+
assert(
|
|
722
|
+
config.includes('moduleFederationPlugin()'),
|
|
723
|
+
`${configPath} must enable Module Federation`,
|
|
724
|
+
);
|
|
725
|
+
assert(
|
|
726
|
+
config.includes("mode: 'stream'") &&
|
|
727
|
+
config.includes('moduleFederationAppSSR: true'),
|
|
728
|
+
`${configPath} must use streaming Module Federation SSR`,
|
|
729
|
+
);
|
|
730
|
+
assert(
|
|
731
|
+
config.includes("html: './'"),
|
|
732
|
+
`${configPath} must keep Modern.js output.distPath.html compatible with Zephyr`,
|
|
733
|
+
);
|
|
734
|
+
assert(
|
|
735
|
+
config.includes("outputStructure: 'flat'"),
|
|
736
|
+
`${configPath} must keep Modern.js HTML output flat for Zephyr`,
|
|
737
|
+
);
|
|
738
|
+
assert(
|
|
739
|
+
config.includes("mainEntryName: 'index'"),
|
|
740
|
+
`${configPath} must keep Modern.js source.mainEntryName compatible with Zephyr`,
|
|
741
|
+
);
|
|
178
742
|
}
|
|
179
743
|
|
|
180
744
|
const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
|
|
181
745
|
assert(shellMf.includes("name: 'shellSuperApp'"), 'Shell MF config must name the shell');
|
|
182
|
-
assert(shellMf.includes('remoteCommerce@http://localhost:3021/mf-manifest.json'), 'Shell must reference commerce remote');
|
|
183
|
-
assert(shellMf.includes('remoteIdentity@http://localhost:3022/mf-manifest.json'), 'Shell must reference identity remote');
|
|
184
|
-
assert(shellMf.includes('remoteDesignSystem@http://localhost:3023/mf-manifest.json'), 'Shell must reference design-system remote');
|
|
185
|
-
|
|
186
|
-
const commerceMf = readText('apps/remotes/remote-commerce/module-federation.config.ts');
|
|
187
|
-
assert(commerceMf.includes("name: 'remoteCommerce'"), 'Commerce remote MF name is missing');
|
|
188
|
-
assert(commerceMf.includes('"./Widget"'), 'Commerce remote must expose a widget');
|
|
189
|
-
|
|
190
|
-
const designMf = readText('apps/remotes/remote-design-system/module-federation.config.ts');
|
|
191
|
-
assert(designMf.includes("name: 'remoteDesignSystem'"), 'Design-system remote MF name is missing');
|
|
192
|
-
assert(designMf.includes('"./Button"'), 'Design-system remote must expose Button');
|
|
193
|
-
assert(designMf.includes('"./tokens"'), 'Design-system remote must expose tokens');
|
|
194
|
-
|
|
195
|
-
const servicePackage = readJson('services/service-recommendations-effect/package.json');
|
|
196
746
|
assert(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
747
|
+
shellMf.includes("'react-dom/client'"),
|
|
748
|
+
'Shell MF config must share react-dom/client explicitly',
|
|
749
|
+
);
|
|
750
|
+
assert(
|
|
751
|
+
shellMf.includes('remoteCommerce@http://localhost:3021/mf-manifest.json'),
|
|
752
|
+
'Shell must reference commerce remote',
|
|
753
|
+
);
|
|
754
|
+
assert(
|
|
755
|
+
shellMf.includes('remoteIdentity@http://localhost:3022/mf-manifest.json'),
|
|
756
|
+
'Shell must reference identity remote',
|
|
200
757
|
);
|
|
201
758
|
assert(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
`Effect service must use @modern-js/app-tools through ${expectedModernDependency('@modern-js/app-tools')}`,
|
|
759
|
+
shellMf.includes('remoteDesignSystem@http://localhost:3023/mf-manifest.json'),
|
|
760
|
+
'Shell must reference design-system remote',
|
|
205
761
|
);
|
|
762
|
+
|
|
763
|
+
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
206
764
|
assert(
|
|
207
|
-
|
|
765
|
+
shellPackage.dependencies?.['@modern-js/plugin-bff'] ===
|
|
208
766
|
expectedModernDependency('@modern-js/plugin-bff'),
|
|
209
|
-
`
|
|
767
|
+
`Shell must use @modern-js/plugin-bff through ${expectedModernDependency(
|
|
768
|
+
'@modern-js/plugin-bff',
|
|
769
|
+
)}`,
|
|
210
770
|
);
|
|
211
771
|
assert(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
'Effect service must link generated shared Effect API through workspace:*',
|
|
772
|
+
!shellPackage.dependencies?.[`@${packageScope}/shared-effect-api`],
|
|
773
|
+
'Shell must not depend on shared-effect-api for default vertical-owned APIs',
|
|
215
774
|
);
|
|
216
775
|
|
|
217
|
-
const
|
|
218
|
-
assert(
|
|
219
|
-
|
|
776
|
+
const generatedContract = readJson('.modernjs/ultramodern-generated-contract.json');
|
|
777
|
+
assert(
|
|
778
|
+
generatedContract.profile === 'cloudflare-ssr-mf-effect-v1',
|
|
779
|
+
'Generated contract must select the Cloudflare SSR MF Effect profile',
|
|
780
|
+
);
|
|
781
|
+
assert(
|
|
782
|
+
generatedContract.packageManager?.manager === 'pnpm' &&
|
|
783
|
+
generatedContract.packageManager?.version === expectedPnpmVersion &&
|
|
784
|
+
generatedContract.packageManager?.toolchain === 'mise',
|
|
785
|
+
'Generated contract must declare the pnpm 11/mise toolchain policy',
|
|
786
|
+
);
|
|
220
787
|
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
assert(
|
|
788
|
+
for (const vertical of fullStackVerticals) {
|
|
789
|
+
const contractEntry = generatedContract.apps?.find((app) => app.id === vertical.id);
|
|
790
|
+
assert(
|
|
791
|
+
contractEntry?.deploy?.target === 'cloudflare' &&
|
|
792
|
+
contractEntry?.deploy?.worker?.ssr === true &&
|
|
793
|
+
contractEntry?.deploy?.output?.flat === true &&
|
|
794
|
+
contractEntry?.deploy?.output?.htmlDistPath === './' &&
|
|
795
|
+
contractEntry?.ssr?.mode === 'stream' &&
|
|
796
|
+
contractEntry?.ssr?.moduleFederationAppSSR === true &&
|
|
797
|
+
contractEntry?.i18n?.plugin === '@modern-js/plugin-i18n' &&
|
|
798
|
+
contractEntry?.i18n?.languages?.includes('en') &&
|
|
799
|
+
contractEntry?.i18n?.languages?.includes('cs') &&
|
|
800
|
+
contractEntry?.moduleFederation?.name === vertical.mfName &&
|
|
801
|
+
contractEntry?.moduleFederation?.exposes?.includes('./Widget') &&
|
|
802
|
+
contractEntry?.moduleFederation?.exposes?.includes('./Route') &&
|
|
803
|
+
contractEntry?.moduleFederation?.browserSafeExposesOnly === true &&
|
|
804
|
+
contractEntry?.effect?.runtime === 'effect' &&
|
|
805
|
+
contractEntry?.effect?.import === '@modern-js/plugin-bff/effect-edge' &&
|
|
806
|
+
contractEntry?.effect?.prefix === vertical.apiPrefix &&
|
|
807
|
+
contractEntry?.effect?.workerEntry === 'worker/__modern_bff_effect.js' &&
|
|
808
|
+
contractEntry?.marker?.appId === vertical.id &&
|
|
809
|
+
contractEntry?.marker?.deployProfile === 'cloudflare-ssr-mf-effect-v1' &&
|
|
810
|
+
contractEntry?.marker?.uiSurface === 'ui' &&
|
|
811
|
+
contractEntry?.marker?.apiSurface === 'effect-bff',
|
|
812
|
+
`${vertical.id} generated contract must describe Cloudflare SSR, MF, i18n, Effect BFF, and marker metadata`,
|
|
813
|
+
);
|
|
814
|
+
}
|
|
224
815
|
|
|
225
|
-
const
|
|
226
|
-
assert(
|
|
816
|
+
const designMf = readText('apps/remotes/remote-design-system/module-federation.config.ts');
|
|
817
|
+
assert(designMf.includes("name: 'remoteDesignSystem'"), 'Design-system remote MF name is missing');
|
|
818
|
+
assert(
|
|
819
|
+
designMf.includes("'react-dom/client'"),
|
|
820
|
+
'Design-system remote MF config must share react-dom/client explicitly',
|
|
821
|
+
);
|
|
822
|
+
assert(designMf.includes("'./Button'"), 'Design-system remote must expose Button');
|
|
823
|
+
assert(designMf.includes("'./tokens'"), 'Design-system remote must expose tokens');
|
|
824
|
+
const sharedEffectApi = readText('packages/shared-effect-api/src/index.ts');
|
|
825
|
+
assert(
|
|
826
|
+
!sharedEffectApi.includes('recommendationsEffectApi') &&
|
|
827
|
+
!sharedEffectApi.includes('commerceEffectApi') &&
|
|
828
|
+
!sharedEffectApi.includes('identityEffectApi'),
|
|
829
|
+
'Shared Effect API package must not own default vertical API contracts',
|
|
830
|
+
);
|
|
227
831
|
|
|
228
832
|
const topology = readJson('topology/reference-topology.json');
|
|
229
833
|
assert(topology.preset === 'presetUltramodern', 'Topology must reference presetUltramodern');
|
|
230
834
|
assert(topology.shell?.id === 'shell-super-app', 'Topology shell id is incorrect');
|
|
231
835
|
assert(topology.shell?.remoteRefs?.length === 3, 'Topology shell must reference three remotes');
|
|
232
836
|
assert(topology.remotes?.length === 3, 'Topology must contain three remotes');
|
|
837
|
+
for (const vertical of fullStackVerticals) {
|
|
838
|
+
const topologyEntry = topology.remotes.find((remote) => remote.id === vertical.id);
|
|
839
|
+
assert(topologyEntry?.kind === 'vertical', `${vertical.id} must be a vertical topology entry`);
|
|
840
|
+
assert(
|
|
841
|
+
topologyEntry?.moduleFederation?.manifestUrl?.includes('/mf-manifest.json') &&
|
|
842
|
+
topologyEntry?.api?.effect?.runtime === 'effect' &&
|
|
843
|
+
topologyEntry?.api?.effect?.bff?.prefix === vertical.apiPrefix &&
|
|
844
|
+
topologyEntry?.api?.effect?.bff?.openapi === '/openapi.json' &&
|
|
845
|
+
topologyEntry?.api?.effect?.contract?.export === './shared/effect/api' &&
|
|
846
|
+
topologyEntry?.api?.effect?.client?.export === './effect/client' &&
|
|
847
|
+
topologyEntry?.api?.effect?.serverEntry === `${vertical.path}/api/effect/index.ts`,
|
|
848
|
+
`${vertical.id} topology entry must include both Module Federation and Effect API metadata`,
|
|
849
|
+
);
|
|
850
|
+
}
|
|
233
851
|
assert(
|
|
234
852
|
topology.remotes.some(
|
|
235
|
-
remote =>
|
|
236
|
-
remote.id === 'remote-design-system' &&
|
|
237
|
-
remote.kind === 'horizontal-design-system',
|
|
853
|
+
(remote) => remote.id === 'remote-design-system' && remote.kind === 'horizontal-design-system',
|
|
238
854
|
),
|
|
239
855
|
'Topology must contain the horizontal design-system remote',
|
|
240
856
|
);
|
|
241
|
-
assert(
|
|
857
|
+
assert(
|
|
858
|
+
(topology.effectServices ?? []).length === 0,
|
|
859
|
+
'Default vertical-owned APIs must not be generated as topology.effectServices',
|
|
860
|
+
);
|
|
242
861
|
assert(topology.sharedPackages?.length === 3, 'Topology must contain shared package placeholders');
|
|
243
862
|
|
|
244
863
|
const ownership = readJson('topology/ownership.json');
|
|
245
864
|
assert(
|
|
246
865
|
ownership.owners?.some(
|
|
247
|
-
owner =>
|
|
248
|
-
owner.id === 'remote-commerce' &&
|
|
249
|
-
owner.ownership?.team === 'commerce-experience',
|
|
866
|
+
(owner) => owner.id === 'remote-commerce' && owner.ownership?.team === 'commerce-experience',
|
|
250
867
|
),
|
|
251
868
|
'Ownership metadata must retain commerce owner',
|
|
252
869
|
);
|
|
253
870
|
assert(
|
|
254
|
-
ownership.owners?.some(
|
|
255
|
-
owner =>
|
|
256
|
-
owner.id === 'service-recommendations-effect'
|
|
257
|
-
owner.
|
|
871
|
+
!ownership.owners?.some(
|
|
872
|
+
(owner) =>
|
|
873
|
+
owner.id === 'service-recommendations-effect' ||
|
|
874
|
+
owner.path === 'services/service-recommendations-effect',
|
|
258
875
|
),
|
|
259
|
-
'Ownership metadata must retain
|
|
876
|
+
'Ownership metadata must not retain the old default recommendations service owner',
|
|
260
877
|
);
|
|
261
878
|
|
|
262
879
|
const manifest = readJson('.modernjs/ultramodern-workspace-template-manifest.json');
|
|
@@ -269,8 +886,47 @@ assert(
|
|
|
269
886
|
'Template manifest must retain the generated package source strategy',
|
|
270
887
|
);
|
|
271
888
|
assert(
|
|
272
|
-
manifest.
|
|
889
|
+
manifest.agentSkills?.source?.commit === rstackAgentSkillsCommit,
|
|
890
|
+
'Template manifest must retain the Rstack agent skills commit',
|
|
891
|
+
);
|
|
892
|
+
assert(
|
|
893
|
+
baselineAgentSkills.every((skillName) => manifest.agentSkills?.baseline?.includes(skillName)),
|
|
894
|
+
'Template manifest must list every baseline agent skill',
|
|
895
|
+
);
|
|
896
|
+
assert(
|
|
897
|
+
manifest.agentSkills?.moduleFederationSource?.repository ===
|
|
898
|
+
'https://github.com/module-federation/agent-skills' &&
|
|
899
|
+
manifest.agentSkills?.moduleFederationSource?.commit ===
|
|
900
|
+
moduleFederationAgentSkillsCommit,
|
|
901
|
+
'Template manifest must retain the Module Federation agent skill source',
|
|
902
|
+
);
|
|
903
|
+
assert(
|
|
904
|
+
moduleFederationAgentSkills.every((skillName) =>
|
|
905
|
+
manifest.agentSkills?.moduleFederationSource?.baseline?.includes(skillName),
|
|
906
|
+
),
|
|
907
|
+
'Template manifest must list every Module Federation agent skill entry',
|
|
908
|
+
);
|
|
909
|
+
assert(
|
|
910
|
+
manifest.agentSkills?.privateSource?.repository === 'https://github.com/TechsioCZ/skills',
|
|
911
|
+
'Template manifest must retain the private TechsioCZ skill source',
|
|
912
|
+
);
|
|
913
|
+
assert(
|
|
914
|
+
privateAgentSkills.every((skillName) =>
|
|
915
|
+
manifest.agentSkills?.privateSource?.baseline?.includes(skillName),
|
|
916
|
+
),
|
|
917
|
+
'Template manifest must list every pinned private agent skill entry',
|
|
918
|
+
);
|
|
919
|
+
assert(
|
|
920
|
+
manifest.validation?.expectedCommands?.includes('mise exec -- pnpm run ultramodern:check'),
|
|
273
921
|
'Template manifest must document the validation command',
|
|
274
922
|
);
|
|
923
|
+
assert(
|
|
924
|
+
manifest.validation?.postMaterializationValidation?.includes('pnpm-11-policy-enforced'),
|
|
925
|
+
'Template manifest must document pnpm 11 policy validation',
|
|
926
|
+
);
|
|
927
|
+
assert(
|
|
928
|
+
manifest.validation?.postMaterializationValidation?.includes('github-workflow-security-enforced'),
|
|
929
|
+
'Template manifest must document generated workflow security validation',
|
|
930
|
+
);
|
|
275
931
|
|
|
276
932
|
console.log('UltraModern workspace scaffold validated');
|