@bleedingdev/modern-js-create 3.2.0-ultramodern.21 → 3.2.0-ultramodern.23
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/skills-lock.json +19 -0
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +1 -1
- package/template-workspace/.mise.toml +2 -0
- package/template-workspace/AGENTS.md +7 -7
- package/template-workspace/README.md.handlebars +12 -3
- 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/validate-ultramodern-workspace.mjs.handlebars +449 -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',
|
|
@@ -331,6 +440,26 @@ for (const skillName of baselineAgentSkills) {
|
|
|
331
440
|
const privateSource = skillsLock.sources?.find(
|
|
332
441
|
(source) => source.repository === 'https://github.com/TechsioCZ/skills',
|
|
333
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
|
+
}
|
|
334
463
|
assert(
|
|
335
464
|
privateSource?.install === 'clone-if-authorized',
|
|
336
465
|
'Agent skills lock must configure TechsioCZ skills as clone-if-authorized',
|
|
@@ -346,18 +475,52 @@ const appPackagePaths = [
|
|
|
346
475
|
'apps/shell-super-app/package.json',
|
|
347
476
|
'apps/remotes/remote-commerce/package.json',
|
|
348
477
|
'apps/remotes/remote-identity/package.json',
|
|
349
|
-
|
|
478
|
+
`${designSystemRemotePath}/package.json`,
|
|
350
479
|
];
|
|
351
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
|
+
|
|
352
487
|
for (const packagePath of appPackagePaths) {
|
|
353
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`);
|
|
354
494
|
assert(
|
|
355
|
-
packageJson.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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`,
|
|
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`,
|
|
360
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
|
+
}
|
|
361
524
|
assert(
|
|
362
525
|
packageJson.dependencies?.['@modern-js/plugin-tanstack'] ===
|
|
363
526
|
expectedModernDependency('@modern-js/plugin-tanstack'),
|
|
@@ -365,6 +528,13 @@ for (const packagePath of appPackagePaths) {
|
|
|
365
528
|
'@modern-js/plugin-tanstack',
|
|
366
529
|
)}`,
|
|
367
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
|
+
);
|
|
368
538
|
assert(
|
|
369
539
|
packageJson.dependencies?.['@modern-js/runtime'] ===
|
|
370
540
|
expectedModernDependency('@modern-js/runtime'),
|
|
@@ -379,6 +549,22 @@ for (const packagePath of appPackagePaths) {
|
|
|
379
549
|
'@modern-js/app-tools',
|
|
380
550
|
)}`,
|
|
381
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
|
+
);
|
|
382
568
|
assert(
|
|
383
569
|
packageJson.dependencies?.[`@${packageScope}/shared-contracts`] === 'workspace:*',
|
|
384
570
|
`${packagePath} must link generated shared contracts through workspace:*`,
|
|
@@ -387,29 +573,131 @@ for (const packagePath of appPackagePaths) {
|
|
|
387
573
|
packageJson.dependencies?.[`@${packageScope}/shared-design-tokens`] === 'workspace:*',
|
|
388
574
|
`${packagePath} must link generated shared design tokens through workspace:*`,
|
|
389
575
|
);
|
|
390
|
-
assert(packageJson.dependencies?.i18next === '26.2.0', `${packagePath} must include i18next`);
|
|
391
576
|
assert(
|
|
392
|
-
packageJson.dependencies?.['
|
|
393
|
-
`${packagePath} must
|
|
577
|
+
packageJson.dependencies?.['node-fetch'] === '^3.3.2',
|
|
578
|
+
`${packagePath} must satisfy the Module Federation SDK node-fetch peer`,
|
|
394
579
|
);
|
|
395
580
|
assert(
|
|
396
581
|
packageJson.dependencies?.['@tanstack/react-router'] === tanstackVersion,
|
|
397
582
|
`${packagePath} must use @tanstack/react-router ${tanstackVersion}`,
|
|
398
583
|
);
|
|
399
584
|
assert(
|
|
400
|
-
packageJson.dependencies?.
|
|
401
|
-
`${packagePath} must include
|
|
585
|
+
packageJson.dependencies?.i18next === '26.2.0',
|
|
586
|
+
`${packagePath} must include i18next for mandatory native Modern.js i18n`,
|
|
587
|
+
);
|
|
588
|
+
assert(
|
|
589
|
+
!packageJson.dependencies?.['react-i18next'],
|
|
590
|
+
`${packagePath} must use native @modern-js/plugin-i18n hooks instead of react-i18next in the TanStack MF starter`,
|
|
402
591
|
);
|
|
403
592
|
assert(
|
|
404
|
-
packageJson.dependencies?.['
|
|
405
|
-
`${packagePath} must include the
|
|
593
|
+
packageJson.dependencies?.['@module-federation/modern-js-v3'] === '2.5.0',
|
|
594
|
+
`${packagePath} must include the Module Federation plugin`,
|
|
406
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
|
+
}
|
|
407
641
|
assert(
|
|
408
642
|
packageJson.modernjs?.preset === 'presetUltramodern',
|
|
409
643
|
`${packagePath} must keep presetUltramodern metadata`,
|
|
410
644
|
);
|
|
411
645
|
}
|
|
412
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
|
+
|
|
413
701
|
for (const configPath of [
|
|
414
702
|
'apps/shell-super-app/modern.config.ts',
|
|
415
703
|
'apps/remotes/remote-commerce/modern.config.ts',
|
|
@@ -418,26 +706,29 @@ for (const configPath of [
|
|
|
418
706
|
]) {
|
|
419
707
|
const config = readText(configPath);
|
|
420
708
|
assert(config.includes('presetUltramodern('), `${configPath} must use presetUltramodern`);
|
|
421
|
-
assert(config.includes('i18nPlugin('), `${configPath} must enable plugin-i18n`);
|
|
422
|
-
assert(config.includes('localePathRedirect: true'), `${configPath} must prefix localized URLs`);
|
|
423
709
|
assert(config.includes('ULTRAMODERN_SITE_URL'), `${configPath} must expose site URL metadata`);
|
|
424
710
|
assert(
|
|
425
711
|
config.includes('MODERN_PUBLIC_SITE_URL must be set for production builds'),
|
|
426
712
|
`${configPath} must require MODERN_PUBLIC_SITE_URL for production builds`,
|
|
427
713
|
);
|
|
428
714
|
assert(config.includes('tanstackRouterPlugin()'), `${configPath} must enable plugin-tanstack`);
|
|
715
|
+
assert(config.includes('i18nPlugin('), `${configPath} must enable native Modern.js i18n`);
|
|
429
716
|
assert(
|
|
430
|
-
config.includes('
|
|
431
|
-
`${configPath} must
|
|
717
|
+
config.includes('reactI18next: false'),
|
|
718
|
+
`${configPath} must use the native Modern.js i18n provider without optional react-i18next probing`,
|
|
432
719
|
);
|
|
433
720
|
assert(
|
|
434
|
-
config.includes("
|
|
435
|
-
`${configPath} must
|
|
721
|
+
config.includes("publicDir: './locales'"),
|
|
722
|
+
`${configPath} must expose app-owned locale JSON through Modern.js publicDir`,
|
|
723
|
+
);
|
|
724
|
+
assert(
|
|
725
|
+
config.includes('moduleFederationPlugin()'),
|
|
726
|
+
`${configPath} must enable Module Federation`,
|
|
436
727
|
);
|
|
437
|
-
assert(config.includes('withZephyr()'), `${configPath} must enable Zephyr through plugins`);
|
|
438
728
|
assert(
|
|
439
|
-
config.includes("
|
|
440
|
-
|
|
729
|
+
config.includes("mode: 'stream'") &&
|
|
730
|
+
config.includes('moduleFederationAppSSR: true'),
|
|
731
|
+
`${configPath} must use streaming Module Federation SSR`,
|
|
441
732
|
);
|
|
442
733
|
assert(
|
|
443
734
|
config.includes("html: './'"),
|
|
@@ -464,18 +755,15 @@ for (const routePath of [
|
|
|
464
755
|
assert(route.includes('rel="alternate"'), `${routePath} must emit alternate locale metadata`);
|
|
465
756
|
assert(route.includes('hrefLang="x-default"'), `${routePath} must emit x-default metadata`);
|
|
466
757
|
assert(route.includes('localizedPath('), `${routePath} must build localized URLs`);
|
|
467
|
-
assert(
|
|
468
|
-
route.includes('@modern-js/plugin-tanstack/runtime'),
|
|
469
|
-
`${routePath} must import TanStack runtime from @modern-js/plugin-tanstack/runtime`,
|
|
470
|
-
);
|
|
471
|
-
assert(
|
|
472
|
-
!route.includes(deprecatedTanstackRuntime),
|
|
473
|
-
`${routePath} must not import deprecated TanStack runtime path`,
|
|
474
|
-
);
|
|
758
|
+
assert(route.includes("import '../index.css';"), `${routePath} must import route CSS`);
|
|
475
759
|
}
|
|
476
760
|
|
|
477
761
|
const shellMf = readText('apps/shell-super-app/module-federation.config.ts');
|
|
478
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
|
+
);
|
|
479
767
|
assert(
|
|
480
768
|
shellMf.includes('remoteCommerce@http://localhost:3021/mf-manifest.json'),
|
|
481
769
|
'Shell must reference commerce remote',
|
|
@@ -489,61 +777,74 @@ assert(
|
|
|
489
777
|
'Shell must reference design-system remote',
|
|
490
778
|
);
|
|
491
779
|
|
|
492
|
-
const
|
|
493
|
-
assert(commerceMf.includes("name: 'remoteCommerce'"), 'Commerce remote MF name is missing');
|
|
494
|
-
assert(commerceMf.includes("'./Widget'"), 'Commerce remote must expose a widget');
|
|
495
|
-
|
|
496
|
-
const designMf = readText('apps/remotes/remote-design-system/module-federation.config.ts');
|
|
497
|
-
assert(designMf.includes("name: 'remoteDesignSystem'"), 'Design-system remote MF name is missing');
|
|
498
|
-
assert(designMf.includes("'./Button'"), 'Design-system remote must expose Button');
|
|
499
|
-
assert(designMf.includes("'./tokens'"), 'Design-system remote must expose tokens');
|
|
500
|
-
|
|
501
|
-
const servicePackage = readJson('services/service-recommendations-effect/package.json');
|
|
502
|
-
assert(
|
|
503
|
-
servicePackage.dependencies?.['@modern-js/runtime'] ===
|
|
504
|
-
expectedModernDependency('@modern-js/runtime'),
|
|
505
|
-
`Effect service must use @modern-js/runtime through ${expectedModernDependency(
|
|
506
|
-
'@modern-js/runtime',
|
|
507
|
-
)}`,
|
|
508
|
-
);
|
|
780
|
+
const shellPackage = readJson('apps/shell-super-app/package.json');
|
|
509
781
|
assert(
|
|
510
|
-
|
|
511
|
-
expectedModernDependency('@modern-js/app-tools'),
|
|
512
|
-
`Effect service must use @modern-js/app-tools through ${expectedModernDependency(
|
|
513
|
-
'@modern-js/app-tools',
|
|
514
|
-
)}`,
|
|
515
|
-
);
|
|
516
|
-
assert(
|
|
517
|
-
servicePackage.devDependencies?.['@modern-js/plugin-bff'] ===
|
|
782
|
+
shellPackage.dependencies?.['@modern-js/plugin-bff'] ===
|
|
518
783
|
expectedModernDependency('@modern-js/plugin-bff'),
|
|
519
|
-
`
|
|
784
|
+
`Shell must use @modern-js/plugin-bff through ${expectedModernDependency(
|
|
520
785
|
'@modern-js/plugin-bff',
|
|
521
786
|
)}`,
|
|
522
787
|
);
|
|
523
788
|
assert(
|
|
524
|
-
|
|
525
|
-
'
|
|
789
|
+
!shellPackage.dependencies?.[`@${packageScope}/shared-effect-api`],
|
|
790
|
+
'Shell must not depend on shared-effect-api for default vertical-owned APIs',
|
|
526
791
|
);
|
|
527
792
|
|
|
528
|
-
const
|
|
793
|
+
const generatedContract = readJson('.modernjs/ultramodern-generated-contract.json');
|
|
529
794
|
assert(
|
|
530
|
-
|
|
531
|
-
'
|
|
795
|
+
generatedContract.profile === 'cloudflare-ssr-mf-effect-v1',
|
|
796
|
+
'Generated contract must select the Cloudflare SSR MF Effect profile',
|
|
532
797
|
);
|
|
533
|
-
assert(serviceConfig.includes('bffPlugin()'), 'Effect service must enable bffPlugin');
|
|
534
|
-
|
|
535
|
-
const serviceEntry = readText('services/service-recommendations-effect/api/effect/index.ts');
|
|
536
798
|
assert(
|
|
537
|
-
|
|
538
|
-
|
|
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',
|
|
539
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');
|
|
540
836
|
assert(
|
|
541
|
-
|
|
542
|
-
'
|
|
837
|
+
designMf.includes("'react-dom/client'"),
|
|
838
|
+
'Design-system remote MF config must share react-dom/client explicitly',
|
|
543
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');
|
|
544
843
|
assert(
|
|
545
|
-
|
|
546
|
-
|
|
844
|
+
!sharedEffectApi.includes('recommendationsEffectApi') &&
|
|
845
|
+
!sharedEffectApi.includes('commerceEffectApi') &&
|
|
846
|
+
!sharedEffectApi.includes('identityEffectApi'),
|
|
847
|
+
'Shared Effect API package must not own default vertical API contracts',
|
|
547
848
|
);
|
|
548
849
|
|
|
549
850
|
const topology = readJson('topology/reference-topology.json');
|
|
@@ -551,6 +852,20 @@ assert(topology.preset === 'presetUltramodern', 'Topology must reference presetU
|
|
|
551
852
|
assert(topology.shell?.id === 'shell-super-app', 'Topology shell id is incorrect');
|
|
552
853
|
assert(topology.shell?.remoteRefs?.length === 3, 'Topology shell must reference three remotes');
|
|
553
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
|
+
}
|
|
554
869
|
assert(
|
|
555
870
|
topology.remotes.some(
|
|
556
871
|
(remote) => remote.id === 'remote-design-system' && remote.kind === 'horizontal-design-system',
|
|
@@ -558,8 +873,8 @@ assert(
|
|
|
558
873
|
'Topology must contain the horizontal design-system remote',
|
|
559
874
|
);
|
|
560
875
|
assert(
|
|
561
|
-
topology.effectServices
|
|
562
|
-
'
|
|
876
|
+
(topology.effectServices ?? []).length === 0,
|
|
877
|
+
'Default vertical-owned APIs must not be generated as topology.effectServices',
|
|
563
878
|
);
|
|
564
879
|
assert(topology.sharedPackages?.length === 3, 'Topology must contain shared package placeholders');
|
|
565
880
|
|
|
@@ -571,12 +886,12 @@ assert(
|
|
|
571
886
|
'Ownership metadata must retain commerce owner',
|
|
572
887
|
);
|
|
573
888
|
assert(
|
|
574
|
-
ownership.owners?.some(
|
|
889
|
+
!ownership.owners?.some(
|
|
575
890
|
(owner) =>
|
|
576
|
-
owner.id === 'service-recommendations-effect'
|
|
577
|
-
owner.
|
|
891
|
+
owner.id === 'service-recommendations-effect' ||
|
|
892
|
+
owner.path === 'services/service-recommendations-effect',
|
|
578
893
|
),
|
|
579
|
-
'Ownership metadata must retain
|
|
894
|
+
'Ownership metadata must not retain the old default recommendations service owner',
|
|
580
895
|
);
|
|
581
896
|
|
|
582
897
|
const manifest = readJson('.modernjs/ultramodern-workspace-template-manifest.json');
|
|
@@ -596,6 +911,19 @@ assert(
|
|
|
596
911
|
baselineAgentSkills.every((skillName) => manifest.agentSkills?.baseline?.includes(skillName)),
|
|
597
912
|
'Template manifest must list every baseline agent skill',
|
|
598
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
|
+
);
|
|
599
927
|
assert(
|
|
600
928
|
manifest.agentSkills?.privateSource?.repository === 'https://github.com/TechsioCZ/skills',
|
|
601
929
|
'Template manifest must retain the private TechsioCZ skill source',
|