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