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