@bleedingdev/modern-js-create 3.4.0-ultramodern.9 → 3.5.0-ultramodern.1

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.
Files changed (135) hide show
  1. package/README.md +49 -23
  2. package/dist/cjs/index.cjs +37 -4
  3. package/dist/cjs/locale/en.cjs +12 -2
  4. package/dist/cjs/locale/zh.cjs +12 -2
  5. package/dist/cjs/ultramodern-tooling/commands.cjs +431 -0
  6. package/dist/cjs/ultramodern-tooling/config.cjs +172 -0
  7. package/dist/cjs/ultramodern-workspace/add-vertical.cjs +87 -75
  8. package/dist/cjs/ultramodern-workspace/{effect-api.cjs → api.cjs} +207 -208
  9. package/dist/cjs/ultramodern-workspace/bridge-config.cjs +327 -0
  10. package/dist/cjs/ultramodern-workspace/codesmith.cjs +1 -0
  11. package/dist/cjs/ultramodern-workspace/contracts.cjs +128 -657
  12. package/dist/cjs/ultramodern-workspace/demo-components.cjs +12 -12
  13. package/dist/cjs/ultramodern-workspace/descriptors.cjs +25 -25
  14. package/dist/cjs/ultramodern-workspace/fs-io.cjs +117 -0
  15. package/dist/cjs/ultramodern-workspace/generation-result.cjs +5 -5
  16. package/dist/cjs/ultramodern-workspace/index.cjs +4 -0
  17. package/dist/cjs/ultramodern-workspace/mf-validation.cjs +571 -0
  18. package/dist/cjs/ultramodern-workspace/module-federation.cjs +39 -12
  19. package/dist/cjs/ultramodern-workspace/overlays.cjs +2 -2
  20. package/dist/cjs/ultramodern-workspace/package-json.cjs +58 -41
  21. package/dist/cjs/ultramodern-workspace/policy.cjs +4 -4
  22. package/dist/cjs/ultramodern-workspace/public-api.cjs +4 -0
  23. package/dist/cjs/ultramodern-workspace/public-surface.cjs +2 -2
  24. package/dist/cjs/ultramodern-workspace/versions.cjs +15 -0
  25. package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +120 -50
  26. package/dist/cjs/ultramodern-workspace/write-workspace.cjs +64 -21
  27. package/dist/esm/index.js +37 -4
  28. package/dist/esm/locale/en.js +12 -2
  29. package/dist/esm/locale/zh.js +12 -2
  30. package/dist/esm/ultramodern-tooling/commands.js +378 -0
  31. package/dist/esm/ultramodern-tooling/config.js +120 -0
  32. package/dist/esm/ultramodern-workspace/add-vertical.js +88 -79
  33. package/dist/esm/ultramodern-workspace/{effect-api.js → api.js} +146 -147
  34. package/dist/esm/ultramodern-workspace/bridge-config.js +270 -0
  35. package/dist/esm/ultramodern-workspace/codesmith.js +1 -0
  36. package/dist/esm/ultramodern-workspace/contracts.js +130 -611
  37. package/dist/esm/ultramodern-workspace/demo-components.js +13 -13
  38. package/dist/esm/ultramodern-workspace/descriptors.js +11 -11
  39. package/dist/esm/ultramodern-workspace/fs-io.js +116 -2
  40. package/dist/esm/ultramodern-workspace/generation-result.js +6 -6
  41. package/dist/esm/ultramodern-workspace/index.js +1 -0
  42. package/dist/esm/ultramodern-workspace/mf-validation.js +516 -0
  43. package/dist/esm/ultramodern-workspace/module-federation.js +40 -13
  44. package/dist/esm/ultramodern-workspace/overlays.js +2 -2
  45. package/dist/esm/ultramodern-workspace/package-json.js +57 -40
  46. package/dist/esm/ultramodern-workspace/policy.js +5 -5
  47. package/dist/esm/ultramodern-workspace/public-api.js +1 -0
  48. package/dist/esm/ultramodern-workspace/public-surface.js +2 -2
  49. package/dist/esm/ultramodern-workspace/versions.js +7 -1
  50. package/dist/esm/ultramodern-workspace/workspace-scripts.js +108 -34
  51. package/dist/esm/ultramodern-workspace/write-workspace.js +66 -26
  52. package/dist/esm-node/index.js +37 -4
  53. package/dist/esm-node/locale/en.js +12 -2
  54. package/dist/esm-node/locale/zh.js +12 -2
  55. package/dist/esm-node/ultramodern-tooling/commands.js +379 -0
  56. package/dist/esm-node/ultramodern-tooling/config.js +121 -0
  57. package/dist/esm-node/ultramodern-workspace/add-vertical.js +88 -79
  58. package/dist/esm-node/ultramodern-workspace/{effect-api.js → api.js} +146 -147
  59. package/dist/esm-node/ultramodern-workspace/bridge-config.js +271 -0
  60. package/dist/esm-node/ultramodern-workspace/codesmith.js +1 -0
  61. package/dist/esm-node/ultramodern-workspace/contracts.js +130 -611
  62. package/dist/esm-node/ultramodern-workspace/demo-components.js +13 -13
  63. package/dist/esm-node/ultramodern-workspace/descriptors.js +11 -11
  64. package/dist/esm-node/ultramodern-workspace/fs-io.js +116 -2
  65. package/dist/esm-node/ultramodern-workspace/generation-result.js +6 -6
  66. package/dist/esm-node/ultramodern-workspace/index.js +1 -0
  67. package/dist/esm-node/ultramodern-workspace/mf-validation.js +517 -0
  68. package/dist/esm-node/ultramodern-workspace/module-federation.js +40 -13
  69. package/dist/esm-node/ultramodern-workspace/overlays.js +2 -2
  70. package/dist/esm-node/ultramodern-workspace/package-json.js +57 -40
  71. package/dist/esm-node/ultramodern-workspace/policy.js +5 -5
  72. package/dist/esm-node/ultramodern-workspace/public-api.js +1 -0
  73. package/dist/esm-node/ultramodern-workspace/public-surface.js +2 -2
  74. package/dist/esm-node/ultramodern-workspace/versions.js +7 -1
  75. package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +108 -34
  76. package/dist/esm-node/ultramodern-workspace/write-workspace.js +66 -26
  77. package/dist/types/locale/en.d.ts +10 -0
  78. package/dist/types/locale/index.d.ts +20 -0
  79. package/dist/types/locale/zh.d.ts +10 -0
  80. package/dist/types/ultramodern-tooling/commands.d.ts +1 -0
  81. package/dist/types/ultramodern-tooling/config.d.ts +43 -0
  82. package/dist/types/ultramodern-workspace/add-vertical.d.ts +5 -2
  83. package/dist/types/ultramodern-workspace/api.d.ts +73 -0
  84. package/dist/types/ultramodern-workspace/bridge-config.d.ts +64 -0
  85. package/dist/types/ultramodern-workspace/codesmith.d.ts +1 -0
  86. package/dist/types/ultramodern-workspace/contracts.d.ts +2 -17
  87. package/dist/types/ultramodern-workspace/descriptors.d.ts +10 -10
  88. package/dist/types/ultramodern-workspace/fs-io.d.ts +1 -0
  89. package/dist/types/ultramodern-workspace/index.d.ts +2 -0
  90. package/dist/types/ultramodern-workspace/mf-validation.d.ts +27 -0
  91. package/dist/types/ultramodern-workspace/package-json.d.ts +5 -4
  92. package/dist/types/ultramodern-workspace/public-api.d.ts +2 -0
  93. package/dist/types/ultramodern-workspace/types.d.ts +9 -7
  94. package/dist/types/ultramodern-workspace/versions.d.ts +6 -0
  95. package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +2 -7
  96. package/dist/types/ultramodern-workspace/write-workspace.d.ts +4 -2
  97. package/package.json +5 -3
  98. package/template-workspace/{.agents → .codex}/skills-lock.json +12 -12
  99. package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +2 -0
  100. package/template-workspace/.gitignore.handlebars +3 -0
  101. package/template-workspace/AGENTS.md.handlebars +11 -5
  102. package/template-workspace/README.md.handlebars +55 -13
  103. package/template-workspace/oxfmt.config.ts +1 -0
  104. package/template-workspace/oxlint.config.ts +1 -0
  105. package/template-workspace/patches/@tanstack__router-core@1.171.13.patch +51 -0
  106. package/template-workspace/pnpm-workspace.yaml.handlebars +7 -0
  107. package/template-workspace/scripts/bootstrap-agent-skills.mjs +77 -67
  108. package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +51 -8
  109. package/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +366 -0
  110. package/templates/workspace-scripts/{check-ultramodern-i18n-boundaries.mjs → check-ultramodern-i18n-boundaries.mts} +1 -1
  111. package/templates/workspace-scripts/generate-public-surface-assets.mjs +246 -9
  112. package/templates/workspace-scripts/proof-cloudflare-version.mjs +277 -9
  113. package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +35 -10
  114. package/templates/workspace-scripts/ultramodern-performance-readiness.mjs +165 -2
  115. package/templates/workspace-scripts/ultramodern-typecheck.mjs +4 -2
  116. package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +842 -84
  117. package/dist/types/ultramodern-workspace/effect-api.d.ts +0 -73
  118. /package/template-workspace/{.agents → .codex}/rstackjs-agent-skills-LICENSE +0 -0
  119. /package/template-workspace/{.agents → .codex}/skills/rsbuild-best-practices/SKILL.md +0 -0
  120. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/SKILL.md +0 -0
  121. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/command-map.md +0 -0
  122. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/common-analysis-patterns.md +0 -0
  123. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-common.md +0 -0
  124. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md +0 -0
  125. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md +0 -0
  126. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor.md +0 -0
  127. /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/rsdoctor-data-types.md +0 -0
  128. /package/template-workspace/{.agents → .codex}/skills/rslib-best-practices/SKILL.md +0 -0
  129. /package/template-workspace/{.agents → .codex}/skills/rslib-modern-package/SKILL.md +0 -0
  130. /package/template-workspace/{.agents → .codex}/skills/rspack-best-practices/SKILL.md +0 -0
  131. /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/SKILL.md +0 -0
  132. /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/bottlenecks.md +0 -0
  133. /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/tracing-guide.md +0 -0
  134. /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/scripts/analyze_trace.js +0 -0
  135. /package/template-workspace/{.agents → .codex}/skills/rstest-best-practices/SKILL.md +0 -0
@@ -2,19 +2,22 @@ import { execFileSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
5
6
 
6
7
  const root = process.cwd();
7
- const lockPath = path.join(root, '.agents/skills-lock.json');
8
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
9
+ const templateWorkspaceDir = path.resolve(scriptDir, '..');
10
+ const vendoredSkillsDir = path.join(templateWorkspaceDir, '.codex/skills');
11
+ const lockPath = path.join(root, '.codex/skills-lock.json');
8
12
  const checkOnly = process.argv.includes('--check');
9
- const force = process.argv.includes('--force');
10
13
  const postinstall = process.argv.includes('--postinstall');
11
14
  const truthy = value => /^(1|true|yes|on)$/i.test(String(value ?? ''));
12
15
  const falsy = value => /^(0|false|no|off)$/i.test(String(value ?? ''));
13
16
  const skipRequested =
14
- truthy(process.env.ULTRAMODERN_SKIP_AGENT_SKILLS) ||
15
- falsy(process.env.ULTRAMODERN_AGENT_SKILLS);
17
+ truthy(process.env.ULTRAMODERN_SKIP_CODEX_SKILLS) ||
18
+ falsy(process.env.ULTRAMODERN_CODEX_SKILLS);
16
19
  const cloneTimeoutMs = Number.parseInt(
17
- process.env.ULTRAMODERN_AGENT_SKILLS_CLONE_TIMEOUT_MS ?? '60000',
20
+ process.env.ULTRAMODERN_CODEX_SKILLS_CLONE_TIMEOUT_MS ?? '60000',
18
21
  10,
19
22
  );
20
23
 
@@ -60,7 +63,7 @@ const requireGit = () => {
60
63
  }
61
64
 
62
65
  throw new Error(
63
- 'Git is required to install agent skills. Install git yourself (for example "brew install git" or "sudo apt-get install git") and run pnpm skills:install again. This script never installs system packages on your behalf.',
66
+ 'Git is required to install clone-backed Codex skills. Install git yourself (for example "brew install git" or "sudo apt-get install git") and run pnpm skills:install again. This script never installs system packages on your behalf.',
64
67
  );
65
68
  };
66
69
 
@@ -172,31 +175,42 @@ const resolveSkillDir = (sourceRoot, skillName) => {
172
175
  };
173
176
 
174
177
  if (!fs.existsSync(lockPath)) {
175
- console.error('Missing .agents/skills-lock.json');
178
+ console.error('Missing .codex/skills-lock.json');
176
179
  process.exit(1);
177
180
  }
178
181
 
179
182
  const lock = readJson(lockPath);
180
- const installDir = path.join(root, lock.installDir ?? '.agents/skills');
183
+ const installDir = path.join(root, lock.installDir ?? '.codex/skills');
181
184
  const sources = lock.sources ?? [];
182
- const requiredCloneSources = sources.filter(
183
- source => source.install === 'clone',
184
- );
185
+ const vendoredSources = sources.filter(source => source.install === 'vendored');
186
+ const cloneSources = sources.filter(source => source.install === 'clone');
185
187
  const optionalCloneSources = sources.filter(
186
188
  source => source.install === 'clone-if-authorized',
187
189
  );
188
- const cloneSourceSkillNames = new Set(
189
- [...requiredCloneSources, ...optionalCloneSources].flatMap(source =>
190
- (source.baseline ?? []).map(skill => skill.name),
190
+ const explicitSourceSkillNames = new Set(
191
+ [...vendoredSources, ...cloneSources, ...optionalCloneSources].flatMap(
192
+ source => (source.baseline ?? []).map(skill => skill.name),
191
193
  ),
192
194
  );
193
- const vendoredRequiredSkills = (lock.baseline ?? []).filter(
194
- skill => !cloneSourceSkillNames.has(skill.name),
195
- );
196
- const cloneOptIn = truthy(process.env.ULTRAMODERN_AGENT_SKILLS);
195
+ const skillsForSource = source =>
196
+ source.baseline ??
197
+ (source.install === 'vendored'
198
+ ? (lock.baseline ?? []).filter(
199
+ skill => !explicitSourceSkillNames.has(skill.name),
200
+ )
201
+ : []);
202
+ const lockedSkillNames = (lock.baseline ?? []).map(skill => skill.name);
203
+ const installedSkillNames = () =>
204
+ lockedSkillNames.filter(skillName =>
205
+ fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
206
+ );
207
+ const missingSkillNames = () =>
208
+ lockedSkillNames.filter(
209
+ skillName => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
210
+ );
197
211
 
198
212
  if (skipRequested) {
199
- const reason = 'agent skills bootstrap skipped by environment';
213
+ const reason = 'Codex skills bootstrap skipped by environment';
200
214
  if (checkOnly) {
201
215
  console.log(reason);
202
216
  process.exit(0);
@@ -207,80 +221,76 @@ if (skipRequested) {
207
221
  }
208
222
 
209
223
  if (checkOnly) {
210
- const missingVendored = vendoredRequiredSkills
211
- .map(skill => skill.name)
212
- .filter(
213
- skillName => !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
214
- );
215
- const missingCloneInstalled = [
216
- ...requiredCloneSources,
217
- ...optionalCloneSources,
218
- ].flatMap(source =>
219
- (source.baseline ?? [])
220
- .map(skill => skill.name)
221
- .filter(
222
- skillName =>
223
- !fs.existsSync(path.join(installDir, skillName, 'SKILL.md')),
224
- ),
225
- );
226
-
227
- if (missingVendored.length > 0) {
228
- console.error(
229
- `Required agent skills not installed: ${missingVendored.join(', ')}. Run pnpm skills:install.`,
230
- );
231
- process.exit(1);
232
- }
233
-
234
- if (missingCloneInstalled.length > 0) {
224
+ const missing = missingSkillNames();
225
+ const installed = installedSkillNames();
226
+ if (missing.length > 0) {
235
227
  console.log(
236
- `Advisory: clone-installed agent skills are not present: ${missingCloneInstalled.join(', ')}. This is expected in CI, nested generated workspaces, and postinstall-only installs; run pnpm skills:install when you need those skills.`,
228
+ `Advisory: pinned Codex skills are not installed: ${missing.join(', ')}. This is expected in offline postinstall runs and fresh check-only CI; run pnpm skills:install when you need local skill bodies.`,
237
229
  );
238
230
  } else {
239
- console.log('All pinned agent skills are installed.');
231
+ console.log('All pinned Codex skills are installed.');
232
+ }
233
+ if (installed.length > 0) {
234
+ console.log(`Installed Codex skills: ${installed.join(', ')}.`);
240
235
  }
241
236
  process.exit(0);
242
237
  }
243
238
 
244
- if (postinstall && !cloneOptIn) {
245
- console.log(
246
- 'Skipping agent skill repository clones during postinstall. Run pnpm skills:install (or set ULTRAMODERN_AGENT_SKILLS=1 before installing) to fetch them.',
247
- );
248
- installLefthook();
249
- process.exit(0);
239
+ fs.mkdirSync(installDir, { recursive: true });
240
+
241
+ const installSkillFromDir = (sourceSkillDir, skillName) => {
242
+ const targetSkillDir = path.join(installDir, skillName);
243
+ if (path.resolve(sourceSkillDir) === path.resolve(targetSkillDir)) {
244
+ console.log(`Pinned Codex skill ${skillName} is already present`);
245
+ return;
246
+ }
247
+ if (fs.existsSync(targetSkillDir)) {
248
+ removeTree(targetSkillDir);
249
+ }
250
+ fs.mkdirSync(path.dirname(targetSkillDir), { recursive: true });
251
+ fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
252
+ console.log(`Installed Codex skill ${skillName}`);
253
+ };
254
+
255
+ for (const source of vendoredSources) {
256
+ for (const skill of skillsForSource(source)) {
257
+ const sourceSkillDir = resolveSkillDir(vendoredSkillsDir, skill.name);
258
+ if (!sourceSkillDir) {
259
+ throw new Error(
260
+ `Vendored Codex skill ${skill.name} not found in ${vendoredSkillsDir}`,
261
+ );
262
+ }
263
+ installSkillFromDir(sourceSkillDir, skill.name);
264
+ }
250
265
  }
251
266
 
252
- fs.mkdirSync(installDir, { recursive: true });
253
- requireGit();
267
+ const cloneInstallSources = postinstall
268
+ ? cloneSources
269
+ : [...cloneSources, ...optionalCloneSources];
254
270
 
255
- for (const source of [...requiredCloneSources, ...optionalCloneSources]) {
271
+ for (const source of cloneInstallSources) {
256
272
  const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ultramodern-skills-'));
257
273
  try {
258
274
  try {
275
+ requireGit();
259
276
  cloneSource(source, tempDir);
260
277
  } catch (error) {
261
278
  if (source.install === 'clone-if-authorized' || postinstall) {
262
- console.warn(`Skipping ${source.repository}; ${error.message}`);
279
+ console.warn(
280
+ `Advisory: unable to install Codex skills from ${source.repository}; ${error.message}. Offline postinstall may continue. Run pnpm skills:install later when network access is available.`,
281
+ );
263
282
  continue;
264
283
  }
265
284
  throw error;
266
285
  }
267
- for (const skill of source.baseline ?? []) {
286
+ for (const skill of skillsForSource(source)) {
268
287
  const sourceSkillDir = resolveSkillDir(tempDir, skill.name);
269
288
  if (!sourceSkillDir) {
270
289
  throw new Error(
271
290
  `Skill ${skill.name} not found in ${source.repository}`,
272
291
  );
273
292
  }
274
- const targetSkillDir = path.join(installDir, skill.name);
275
- if (fs.existsSync(targetSkillDir)) {
276
- if (!force) {
277
- console.log(`Skipping existing ${skill.name}`);
278
- continue;
279
- }
280
- removeTree(targetSkillDir);
281
- }
282
- fs.cpSync(sourceSkillDir, targetSkillDir, { recursive: true });
283
- console.log(`Installed ${skill.name}`);
293
+ installSkillFromDir(sourceSkillDir, skill.name);
284
294
  }
285
295
  } finally {
286
296
  removeTree(tempDir);
@@ -2,19 +2,62 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
4
  const root = process.cwd();
5
- const generatedContractPath = path.join(
6
- root,
7
- '.modernjs/ultramodern-generated-contract.json',
8
- );
9
- const generatedContract = fs.existsSync(generatedContractPath)
10
- ? JSON.parse(fs.readFileSync(generatedContractPath, 'utf-8'))
11
- : undefined;
5
+ const compactConfigPath = path.join(root, '.modernjs/ultramodern.json');
12
6
  const defaultAppDirs = {{defaultAppDirsJson}};
13
7
 
8
+ function readJson(filePath) {
9
+ return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
10
+ }
11
+
12
+ function normalizeRelativePath(value) {
13
+ return String(value ?? '').replace(/\\/gu, '/').replace(/^\.\/+/u, '');
14
+ }
15
+
16
+ function readGeneratedContractView() {
17
+ if (fs.existsSync(compactConfigPath)) {
18
+ const compactConfig = readJson(compactConfigPath);
19
+ return {
20
+ sourcePath: compactConfigPath,
21
+ apps: Array.isArray(compactConfig.topology?.apps)
22
+ ? compactConfig.topology.apps.map(app => {
23
+ const moduleFederation =
24
+ app.moduleFederation && typeof app.moduleFederation === 'object'
25
+ ? app.moduleFederation
26
+ : {};
27
+ return {
28
+ id: String(app.id),
29
+ path:
30
+ typeof app.path === 'string'
31
+ ? normalizeRelativePath(app.path)
32
+ : '.',
33
+ moduleFederation: {
34
+ exposes: Array.isArray(moduleFederation.exposes)
35
+ ? moduleFederation.exposes.filter(
36
+ expose => typeof expose === 'string',
37
+ )
38
+ : [],
39
+ dts: {
40
+ compilerInstance:
41
+ moduleFederation.dts?.compilerInstance ?? 'tsgo',
42
+ tsConfigPath:
43
+ moduleFederation.dts?.tsConfigPath ??
44
+ './tsconfig.mf-types.json',
45
+ },
46
+ },
47
+ };
48
+ })
49
+ : [],
50
+ };
51
+ }
52
+ throw new Error('Missing UltraModern config. Expected .modernjs/ultramodern.json.');
53
+ }
54
+
55
+ const generatedContract = readGeneratedContractView();
56
+
14
57
  const args = process.argv.slice(2);
15
58
  if (args.includes('--help') || args.includes('-h')) {
16
59
  process.stdout.write(`Usage:
17
- node scripts/assert-mf-types.mjs [app-dir...]
60
+ node scripts/assert-mf-types.mts [app-dir...]
18
61
 
19
62
  Checks that every Module Federation remote with exposed modules emitted a non-empty dist/@mf-types.zip archive and uses the workspace TypeScript compiler.
20
63
  `);
@@ -0,0 +1,366 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const workspaceRoot = process.env.ULTRAMODERN_WORKSPACE_ROOT ?? process.cwd();
6
+ const failures = [];
7
+
8
+ const ignoredDirectories = new Set([
9
+ '.git',
10
+ '.modern',
11
+ '.output',
12
+ 'coverage',
13
+ 'dist',
14
+ 'node_modules',
15
+ 'repos',
16
+ ]);
17
+
18
+ function normalize(filePath) {
19
+ return filePath.split(path.sep).join('/');
20
+ }
21
+
22
+ function relative(filePath) {
23
+ return normalize(path.relative(workspaceRoot, filePath));
24
+ }
25
+
26
+ function exists(relativePath) {
27
+ return fs.existsSync(path.join(workspaceRoot, relativePath));
28
+ }
29
+
30
+ function readText(relativePath) {
31
+ return fs.readFileSync(path.join(workspaceRoot, relativePath), 'utf8');
32
+ }
33
+
34
+ function fail(message) {
35
+ failures.push(message);
36
+ }
37
+
38
+ function assert(condition, message) {
39
+ if (!condition) {
40
+ fail(message);
41
+ }
42
+ }
43
+
44
+ function listFiles(startDirectory) {
45
+ const absoluteStart = path.join(workspaceRoot, startDirectory);
46
+ if (!fs.existsSync(absoluteStart)) {
47
+ return [];
48
+ }
49
+
50
+ const files = [];
51
+ const visit = directory => {
52
+ for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
53
+ if (ignoredDirectories.has(entry.name)) {
54
+ continue;
55
+ }
56
+
57
+ const absoluteEntry = path.join(directory, entry.name);
58
+ if (entry.isDirectory()) {
59
+ visit(absoluteEntry);
60
+ continue;
61
+ }
62
+
63
+ if (entry.isFile()) {
64
+ files.push(relative(absoluteEntry));
65
+ }
66
+ }
67
+ };
68
+
69
+ visit(absoluteStart);
70
+ return files;
71
+ }
72
+
73
+ function listDirectories(startDirectory) {
74
+ const absoluteStart = path.join(workspaceRoot, startDirectory);
75
+ if (!fs.existsSync(absoluteStart)) {
76
+ return [];
77
+ }
78
+
79
+ return fs
80
+ .readdirSync(absoluteStart, { withFileTypes: true })
81
+ .filter(entry => entry.isDirectory() && !ignoredDirectories.has(entry.name))
82
+ .map(entry => path.posix.join(startDirectory, entry.name));
83
+ }
84
+
85
+ function assertNoPath(relativePath, message) {
86
+ if (exists(relativePath)) {
87
+ fail(message);
88
+ }
89
+ }
90
+
91
+ function assertContains(relativePath, content, pattern, message) {
92
+ assert(pattern.test(content), `${relativePath}: ${message}`);
93
+ }
94
+
95
+ function assertNotContains(relativePath, content, pattern, message) {
96
+ assert(!pattern.test(content), `${relativePath}: ${message}`);
97
+ }
98
+
99
+ for (const forbiddenPath of [
100
+ ...listDirectories('apps').flatMap(appPath => [
101
+ `${appPath}/api/effect`,
102
+ `${appPath}/api/lambda`,
103
+ `${appPath}/shared/effect`,
104
+ `${appPath}/src/effect`,
105
+ ]),
106
+ ...listDirectories('verticals').flatMap(verticalPath => [
107
+ `${verticalPath}/api/effect`,
108
+ `${verticalPath}/api/lambda`,
109
+ `${verticalPath}/shared/effect`,
110
+ `${verticalPath}/src/effect`,
111
+ ]),
112
+ ]) {
113
+ assertNoPath(
114
+ forbiddenPath,
115
+ `${forbiddenPath} is forbidden in UltraModern strictEffectApproach workspaces; use api/index.ts, shared/api.ts and src/api/* instead.`,
116
+ );
117
+ }
118
+
119
+ const generatedFiles = [
120
+ ...listFiles('apps'),
121
+ ...listFiles('verticals'),
122
+ ...listFiles('packages'),
123
+ ];
124
+ const textFiles = generatedFiles.filter(file =>
125
+ /\.(?:[cm]?[jt]sx?|json|md|mjs|mts|cts)$/u.test(file),
126
+ );
127
+
128
+ for (const file of textFiles) {
129
+ const content = readText(file);
130
+
131
+ if (/\/api\//u.test(file)) {
132
+ assertNotContains(
133
+ file,
134
+ content,
135
+ /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u,
136
+ 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.',
137
+ );
138
+ assertNotContains(
139
+ file,
140
+ content,
141
+ /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u,
142
+ 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.',
143
+ );
144
+ assertNotContains(
145
+ file,
146
+ content,
147
+ /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u,
148
+ 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.',
149
+ );
150
+ assertNotContains(
151
+ file,
152
+ content,
153
+ /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u,
154
+ 'API modules must not define unbranded handler factories; use defineEffectBff(...).',
155
+ );
156
+ assertNotContains(
157
+ file,
158
+ content,
159
+ /\bSchema\.(?:UnknownFromJsonString|Unknown|Any)\b/u,
160
+ 'API modules must use concrete request, response and error schemas; Schema.UnknownFromJsonString, Schema.Unknown and Schema.Any are forbidden in UltraModern API code.',
161
+ );
162
+ }
163
+
164
+ assertNotContains(
165
+ file,
166
+ content,
167
+ /@modern-js\/plugin-bff\/hono-server/u,
168
+ 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.',
169
+ );
170
+ assertNotContains(
171
+ file,
172
+ content,
173
+ /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u,
174
+ 'Generated UltraModern API apps must use the Effect runtime.',
175
+ );
176
+ assertNotContains(
177
+ file,
178
+ content,
179
+ /\bstrictEffectApproach\s*(?::|=)\s*false\b/u,
180
+ 'Generated UltraModern API apps must keep strictEffectApproach enabled.',
181
+ );
182
+ }
183
+
184
+ const verticalDirectories = listDirectories('verticals');
185
+ const shellClient = 'apps/shell-super-app/src/api/vertical-clients.ts';
186
+ if (exists('apps/shell-super-app') && verticalDirectories.length > 0) {
187
+ assert(exists(shellClient), `${shellClient} must aggregate vertical API clients.`);
188
+ }
189
+
190
+ function assertApiSurface(appPath) {
191
+ const apiEntry = `${appPath}/api/index.ts`;
192
+ const sharedApi = `${appPath}/shared/api.ts`;
193
+ const srcApiDirectory = `${appPath}/src/api`;
194
+ const modernConfig = `${appPath}/modern.config.ts`;
195
+ const packageJsonPath = `${appPath}/package.json`;
196
+
197
+ assert(exists(apiEntry), `${apiEntry} is required.`);
198
+ assert(exists(sharedApi), `${sharedApi} is required.`);
199
+ assert(exists(srcApiDirectory), `${srcApiDirectory} is required.`);
200
+
201
+ if (exists(srcApiDirectory)) {
202
+ const clientFiles = listFiles(srcApiDirectory).filter(file =>
203
+ /-client\.ts$/u.test(file),
204
+ );
205
+ assert(
206
+ clientFiles.length > 0,
207
+ `${srcApiDirectory} must contain a generated API client.`,
208
+ );
209
+ }
210
+
211
+ if (exists(apiEntry)) {
212
+ const entry = readText(apiEntry);
213
+ assertContains(
214
+ apiEntry,
215
+ entry,
216
+ /\bdefineEffectBff\b/u,
217
+ 'must export a defineEffectBff(...) runtime definition.',
218
+ );
219
+ assertContains(
220
+ apiEntry,
221
+ entry,
222
+ /\bHttpApiBuilder\b/u,
223
+ 'must implement handlers through HttpApiBuilder.',
224
+ );
225
+ assertContains(
226
+ apiEntry,
227
+ entry,
228
+ /\bLayer\b/u,
229
+ 'must compose dependencies with Effect Layer.',
230
+ );
231
+ assertContains(
232
+ apiEntry,
233
+ entry,
234
+ /from ['"]\.\.\/shared\/api\.ts['"]/u,
235
+ 'must import the contract from ../shared/api.ts.',
236
+ );
237
+ }
238
+
239
+ if (exists(sharedApi)) {
240
+ const contract = readText(sharedApi);
241
+ assertContains(
242
+ sharedApi,
243
+ contract,
244
+ /\bHttpApi\.make\b/u,
245
+ 'must declare the HttpApi contract.',
246
+ );
247
+ assertContains(
248
+ sharedApi,
249
+ contract,
250
+ /\bHttpApiGroup\.make\b/u,
251
+ 'must declare HttpApi groups.',
252
+ );
253
+ assertContains(
254
+ sharedApi,
255
+ contract,
256
+ /\bHttpApiEndpoint\./u,
257
+ 'must declare endpoints through HttpApiEndpoint.',
258
+ );
259
+ assertContains(
260
+ sharedApi,
261
+ contract,
262
+ /\bSchema\./u,
263
+ 'must use Schema for request, response and error shapes.',
264
+ );
265
+ }
266
+
267
+ if (exists(modernConfig)) {
268
+ const config = readText(modernConfig);
269
+ assertContains(
270
+ modernConfig,
271
+ config,
272
+ /runtimeFramework:\s*['"]effect['"]/u,
273
+ 'must use bff.runtimeFramework: effect.',
274
+ );
275
+ assertContains(
276
+ modernConfig,
277
+ config,
278
+ /entry:\s*['"]\.\/api\/index['"]/u,
279
+ 'must point bff.effect.entry at ./api/index.',
280
+ );
281
+ assertContains(
282
+ modernConfig,
283
+ config,
284
+ /strictEffectApproach:\s*true/u,
285
+ 'must enable strictEffectApproach explicitly.',
286
+ );
287
+ }
288
+
289
+ if (exists(packageJsonPath)) {
290
+ const packageJson = JSON.parse(readText(packageJsonPath));
291
+ assert(
292
+ packageJson.exports?.['./api'] === './shared/api.ts',
293
+ `${packageJsonPath}: package must export ./api from shared/api.ts.`,
294
+ );
295
+ assert(
296
+ typeof packageJson.exports?.['./api/client'] === 'string' &&
297
+ packageJson.exports['./api/client'].startsWith('./src/api/'),
298
+ `${packageJsonPath}: package must export ./api/client from src/api/*.`,
299
+ );
300
+ }
301
+ }
302
+
303
+ for (const appPath of listDirectories('apps')) {
304
+ if (exists(`${appPath}/api/index.ts`) || exists(`${appPath}/shared/api.ts`)) {
305
+ assertApiSurface(appPath);
306
+ }
307
+ }
308
+
309
+ for (const verticalPath of verticalDirectories) {
310
+ assertApiSurface(verticalPath);
311
+ }
312
+
313
+ if (exists('apps/shell-super-app/package.json')) {
314
+ const shellPackageJson = JSON.parse(
315
+ readText('apps/shell-super-app/package.json'),
316
+ );
317
+ assert(
318
+ shellPackageJson.exports?.['./api/clients'] ===
319
+ './src/api/vertical-clients.ts',
320
+ 'apps/shell-super-app/package.json must export ./api/clients.',
321
+ );
322
+ }
323
+
324
+ if (exists('package.json')) {
325
+ const rootPackageJson = JSON.parse(readText('package.json'));
326
+ assert(
327
+ rootPackageJson.scripts?.['api:check'] ===
328
+ 'node ./scripts/check-ultramodern-api-boundaries.mts',
329
+ 'Root package.json must expose api:check.',
330
+ );
331
+ assert(
332
+ rootPackageJson.scripts?.check?.includes('pnpm api:check'),
333
+ 'Root check script must include pnpm api:check.',
334
+ );
335
+ }
336
+
337
+ if (exists('topology/reference-topology.json')) {
338
+ const topology = JSON.parse(readText('topology/reference-topology.json'));
339
+ for (const vertical of topology.verticals ?? []) {
340
+ if (vertical.api?.runtime === 'effect') {
341
+ assert(
342
+ vertical.api.bff?.strictEffectApproach === true,
343
+ `${vertical.id} topology must mark strictEffectApproach as true.`,
344
+ );
345
+ assert(
346
+ typeof vertical.api.serverEntry === 'string' &&
347
+ vertical.api.serverEntry.endsWith('/api/index.ts'),
348
+ `${vertical.id} topology must use api/index.ts as the server entry.`,
349
+ );
350
+ }
351
+ assert(
352
+ !vertical.api?.effect,
353
+ `${vertical.id} topology must describe the API directly, not under api.effect.`,
354
+ );
355
+ }
356
+ }
357
+
358
+ if (failures.length > 0) {
359
+ console.error('UltraModern API boundary check failed:');
360
+ for (const failure of failures) {
361
+ console.error(`- ${failure}`);
362
+ }
363
+ process.exit(1);
364
+ }
365
+
366
+ console.log('UltraModern API boundary check passed.');
@@ -5,5 +5,5 @@ import { runWorkspaceSourceCheck } from '@modern-js/code-tools';
5
5
  const root = path.resolve(import.meta.dirname, '..');
6
6
  process.exitCode = runWorkspaceSourceCheck({
7
7
  cwd: root,
8
- sourceRoots: ['apps', 'verticals'],
8
+ sourceRoots: ['apps', 'verticals', 'packages'],
9
9
  });