@bleedingdev/modern-js-create 3.4.0-ultramodern.8 → 3.5.0-ultramodern.0

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 +25 -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 +185 -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 +127 -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 +33 -18
  19. package/dist/cjs/ultramodern-workspace/overlays.cjs +2 -2
  20. package/dist/cjs/ultramodern-workspace/package-json.cjs +57 -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 +5 -0
  25. package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +119 -50
  26. package/dist/cjs/ultramodern-workspace/write-workspace.cjs +62 -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 +132 -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 +129 -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 +34 -19
  44. package/dist/esm/ultramodern-workspace/overlays.js +2 -2
  45. package/dist/esm/ultramodern-workspace/package-json.js +56 -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 +3 -1
  50. package/dist/esm/ultramodern-workspace/workspace-scripts.js +107 -34
  51. package/dist/esm/ultramodern-workspace/write-workspace.js +64 -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 +133 -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 +129 -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 +34 -19
  69. package/dist/esm-node/ultramodern-workspace/overlays.js +2 -2
  70. package/dist/esm-node/ultramodern-workspace/package-json.js +56 -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 +3 -1
  75. package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +107 -34
  76. package/dist/esm-node/ultramodern-workspace/write-workspace.js +64 -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 +2 -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 +15 -12
  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 +4 -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 +344 -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 +840 -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
@@ -0,0 +1,344 @@
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
+ 'apps/shell-super-app/src/effect',
101
+ ...listDirectories('verticals').flatMap(verticalPath => [
102
+ `${verticalPath}/api/effect`,
103
+ `${verticalPath}/api/lambda`,
104
+ `${verticalPath}/shared/effect`,
105
+ `${verticalPath}/src/effect`,
106
+ ]),
107
+ ]) {
108
+ assertNoPath(
109
+ forbiddenPath,
110
+ `${forbiddenPath} is forbidden in UltraModern strictEffectApproach workspaces; use api/index.ts, shared/api.ts and src/api/* instead.`,
111
+ );
112
+ }
113
+
114
+ const generatedFiles = [
115
+ ...listFiles('apps'),
116
+ ...listFiles('verticals'),
117
+ ...listFiles('packages'),
118
+ ];
119
+ const textFiles = generatedFiles.filter(file =>
120
+ /\.(?:[cm]?[jt]sx?|json|md|mjs|mts|cts)$/u.test(file),
121
+ );
122
+
123
+ for (const file of textFiles) {
124
+ const content = readText(file);
125
+
126
+ if (/\/api\//u.test(file)) {
127
+ assertNotContains(
128
+ file,
129
+ content,
130
+ /\bnew\s+Response\s*\(|\bResponse\.json\s*\(/u,
131
+ 'API modules must not hand-build Response objects; model endpoints through Effect HttpApi and schemas.',
132
+ );
133
+ assertNotContains(
134
+ file,
135
+ content,
136
+ /\b(?:request|req)\.(?:json|text|formData|arrayBuffer)\s*\(/u,
137
+ 'API modules must not manually parse request bodies; use HttpApiEndpoint payload/query/params schemas.',
138
+ );
139
+ assertNotContains(
140
+ file,
141
+ content,
142
+ /\bexport\s+const\s+handler\b|\bexport\s+default\s+async\b/u,
143
+ 'API modules must not export raw request handlers; export defineEffectBff(...) from api/index.ts.',
144
+ );
145
+ assertNotContains(
146
+ file,
147
+ content,
148
+ /\bcreateHandler\s*[:=]\s*(?!defineEffectBff\b)/u,
149
+ 'API modules must not define unbranded handler factories; use defineEffectBff(...).',
150
+ );
151
+ }
152
+
153
+ assertNotContains(
154
+ file,
155
+ content,
156
+ /@modern-js\/plugin-bff\/hono-server/u,
157
+ 'UltraModern API workspaces must not import Hono server helpers; use @modern-js/plugin-bff/effect-edge and HttpApi.',
158
+ );
159
+ assertNotContains(
160
+ file,
161
+ content,
162
+ /\bruntimeFramework\s*(?::|=)\s*['"]hono['"]/u,
163
+ 'Generated UltraModern API apps must use the Effect runtime.',
164
+ );
165
+ assertNotContains(
166
+ file,
167
+ content,
168
+ /\bstrictEffectApproach\s*(?::|=)\s*false\b/u,
169
+ 'Generated UltraModern API apps must keep strictEffectApproach enabled.',
170
+ );
171
+ }
172
+
173
+ const shellClient = 'apps/shell-super-app/src/api/vertical-clients.ts';
174
+ if (exists('apps/shell-super-app')) {
175
+ assert(exists(shellClient), `${shellClient} must aggregate vertical API clients.`);
176
+ }
177
+
178
+ for (const verticalPath of listDirectories('verticals')) {
179
+ const apiEntry = `${verticalPath}/api/index.ts`;
180
+ const sharedApi = `${verticalPath}/shared/api.ts`;
181
+ const srcApiDirectory = `${verticalPath}/src/api`;
182
+ const modernConfig = `${verticalPath}/modern.config.ts`;
183
+ const packageJsonPath = `${verticalPath}/package.json`;
184
+
185
+ assert(exists(apiEntry), `${apiEntry} is required.`);
186
+ assert(exists(sharedApi), `${sharedApi} is required.`);
187
+ assert(exists(srcApiDirectory), `${srcApiDirectory} is required.`);
188
+
189
+ if (exists(srcApiDirectory)) {
190
+ const clientFiles = listFiles(srcApiDirectory).filter(file =>
191
+ /-client\.ts$/u.test(file),
192
+ );
193
+ assert(
194
+ clientFiles.length > 0,
195
+ `${srcApiDirectory} must contain a generated API client.`,
196
+ );
197
+ }
198
+
199
+ if (exists(apiEntry)) {
200
+ const entry = readText(apiEntry);
201
+ assertContains(
202
+ apiEntry,
203
+ entry,
204
+ /\bdefineEffectBff\b/u,
205
+ 'must export a defineEffectBff(...) runtime definition.',
206
+ );
207
+ assertContains(
208
+ apiEntry,
209
+ entry,
210
+ /\bHttpApiBuilder\b/u,
211
+ 'must implement handlers through HttpApiBuilder.',
212
+ );
213
+ assertContains(
214
+ apiEntry,
215
+ entry,
216
+ /\bLayer\b/u,
217
+ 'must compose dependencies with Effect Layer.',
218
+ );
219
+ assertContains(
220
+ apiEntry,
221
+ entry,
222
+ /from ['"]\.\.\/shared\/api\.ts['"]/u,
223
+ 'must import the contract from ../shared/api.ts.',
224
+ );
225
+ }
226
+
227
+ if (exists(sharedApi)) {
228
+ const contract = readText(sharedApi);
229
+ assertContains(
230
+ sharedApi,
231
+ contract,
232
+ /\bHttpApi\.make\b/u,
233
+ 'must declare the HttpApi contract.',
234
+ );
235
+ assertContains(
236
+ sharedApi,
237
+ contract,
238
+ /\bHttpApiGroup\.make\b/u,
239
+ 'must declare HttpApi groups.',
240
+ );
241
+ assertContains(
242
+ sharedApi,
243
+ contract,
244
+ /\bHttpApiEndpoint\./u,
245
+ 'must declare endpoints through HttpApiEndpoint.',
246
+ );
247
+ assertContains(
248
+ sharedApi,
249
+ contract,
250
+ /\bSchema\./u,
251
+ 'must use Schema for request, response and error shapes.',
252
+ );
253
+ }
254
+
255
+ if (exists(modernConfig)) {
256
+ const config = readText(modernConfig);
257
+ assertContains(
258
+ modernConfig,
259
+ config,
260
+ /runtimeFramework:\s*['"]effect['"]/u,
261
+ 'must use bff.runtimeFramework: effect.',
262
+ );
263
+ assertContains(
264
+ modernConfig,
265
+ config,
266
+ /entry:\s*['"]\.\/api\/index['"]/u,
267
+ 'must point bff.effect.entry at ./api/index.',
268
+ );
269
+ assertContains(
270
+ modernConfig,
271
+ config,
272
+ /strictEffectApproach:\s*true/u,
273
+ 'must enable strictEffectApproach explicitly.',
274
+ );
275
+ }
276
+
277
+ if (exists(packageJsonPath)) {
278
+ const packageJson = JSON.parse(readText(packageJsonPath));
279
+ assert(
280
+ packageJson.exports?.['./api'] === './shared/api.ts',
281
+ `${packageJsonPath}: package must export ./api from shared/api.ts.`,
282
+ );
283
+ assert(
284
+ typeof packageJson.exports?.['./api/client'] === 'string' &&
285
+ packageJson.exports['./api/client'].startsWith('./src/api/'),
286
+ `${packageJsonPath}: package must export ./api/client from src/api/*.`,
287
+ );
288
+ }
289
+ }
290
+
291
+ if (exists('apps/shell-super-app/package.json')) {
292
+ const shellPackageJson = JSON.parse(
293
+ readText('apps/shell-super-app/package.json'),
294
+ );
295
+ assert(
296
+ shellPackageJson.exports?.['./api/clients'] ===
297
+ './src/api/vertical-clients.ts',
298
+ 'apps/shell-super-app/package.json must export ./api/clients.',
299
+ );
300
+ }
301
+
302
+ if (exists('package.json')) {
303
+ const rootPackageJson = JSON.parse(readText('package.json'));
304
+ assert(
305
+ rootPackageJson.scripts?.['api:check'] ===
306
+ 'node ./scripts/check-ultramodern-api-boundaries.mts',
307
+ 'Root package.json must expose api:check.',
308
+ );
309
+ assert(
310
+ rootPackageJson.scripts?.check?.includes('pnpm api:check'),
311
+ 'Root check script must include pnpm api:check.',
312
+ );
313
+ }
314
+
315
+ if (exists('topology/reference-topology.json')) {
316
+ const topology = JSON.parse(readText('topology/reference-topology.json'));
317
+ for (const vertical of topology.verticals ?? []) {
318
+ if (vertical.api?.runtime === 'effect') {
319
+ assert(
320
+ vertical.api.bff?.strictEffectApproach === true,
321
+ `${vertical.id} topology must mark strictEffectApproach as true.`,
322
+ );
323
+ assert(
324
+ typeof vertical.api.serverEntry === 'string' &&
325
+ vertical.api.serverEntry.endsWith('/api/index.ts'),
326
+ `${vertical.id} topology must use api/index.ts as the server entry.`,
327
+ );
328
+ }
329
+ assert(
330
+ !vertical.api?.effect,
331
+ `${vertical.id} topology must describe the API directly, not under api.effect.`,
332
+ );
333
+ }
334
+ }
335
+
336
+ if (failures.length > 0) {
337
+ console.error('UltraModern API boundary check failed:');
338
+ for (const failure of failures) {
339
+ console.error(`- ${failure}`);
340
+ }
341
+ process.exit(1);
342
+ }
343
+
344
+ 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
  });
@@ -4,18 +4,255 @@ import path from 'node:path';
4
4
  import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
 
6
6
  const workspaceRoot = path.resolve(
7
- path.dirname(fileURLToPath(import.meta.url)),
8
- '..',
9
- );
10
- const contractPath = path.join(
11
- workspaceRoot,
12
- '.modernjs/ultramodern-generated-contract.json',
7
+ process.env.ULTRAMODERN_WORKSPACE_ROOT ??
8
+ path.join(path.dirname(fileURLToPath(import.meta.url)), '..'),
13
9
  );
10
+ const compactConfigPath = path.join(workspaceRoot, '.modernjs/ultramodern.json');
14
11
 
15
12
  function readJson(filePath) {
16
13
  return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
17
14
  }
18
15
 
16
+ function toKebabCase(value) {
17
+ return String(value)
18
+ .trim()
19
+ .replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
20
+ .replace(/[^a-zA-Z0-9._-]+/gu, '-')
21
+ .replace(/[._]+/gu, '-')
22
+ .toLowerCase()
23
+ .replace(/-+/gu, '-')
24
+ .replace(/^-+|-+$/gu, '');
25
+ }
26
+
27
+ function toEnvSegment(value) {
28
+ return toKebabCase(value).replace(/-/gu, '_').toUpperCase();
29
+ }
30
+
31
+ function normalizeRelativePath(value) {
32
+ return String(value ?? '').replace(/\\/gu, '/').replace(/^\.\/+/u, '');
33
+ }
34
+
35
+ function appNamespace(app) {
36
+ return app.kind === 'shell' ? 'shell' : (app.domain ?? app.id);
37
+ }
38
+
39
+ function defaultPortEnv(app) {
40
+ return app.kind === 'shell'
41
+ ? 'SHELL_SUPER_APP_PORT'
42
+ : `VERTICAL_${toEnvSegment(app.domain ?? app.id)}_PORT`;
43
+ }
44
+
45
+ function defaultPort(app) {
46
+ return app.kind === 'shell' ? 3020 : 3030;
47
+ }
48
+
49
+ function normalizeCompactApp(rawApp) {
50
+ const id = String(rawApp.id);
51
+ const kind = rawApp.kind === 'vertical' ? 'vertical' : 'shell';
52
+ const appPath =
53
+ typeof rawApp.path === 'string'
54
+ ? normalizeRelativePath(rawApp.path)
55
+ : kind === 'shell'
56
+ ? 'apps/shell-super-app'
57
+ : `verticals/${toKebabCase(id)}`;
58
+ const packageSuffix =
59
+ typeof rawApp.packageSuffix === 'string'
60
+ ? rawApp.packageSuffix
61
+ : appPath.split('/').at(-1) ?? id;
62
+ const domain =
63
+ typeof rawApp.domain === 'string'
64
+ ? rawApp.domain
65
+ : kind === 'vertical'
66
+ ? packageSuffix
67
+ : undefined;
68
+ const moduleFederation =
69
+ rawApp.moduleFederation && typeof rawApp.moduleFederation === 'object'
70
+ ? rawApp.moduleFederation
71
+ : {};
72
+
73
+ return {
74
+ id,
75
+ kind,
76
+ path: appPath,
77
+ packageSuffix,
78
+ domain,
79
+ port:
80
+ typeof rawApp.port === 'number'
81
+ ? rawApp.port
82
+ : defaultPort({ id, kind, domain }),
83
+ portEnv:
84
+ typeof rawApp.portEnv === 'string'
85
+ ? rawApp.portEnv
86
+ : defaultPortEnv({ id, kind, domain }),
87
+ mfName:
88
+ typeof moduleFederation.name === 'string'
89
+ ? moduleFederation.name
90
+ : kind === 'shell'
91
+ ? 'shellSuperApp'
92
+ : `vertical${toKebabCase(domain ?? id)
93
+ .split('-')
94
+ .filter(Boolean)
95
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1))
96
+ .join('')}`,
97
+ exposes: Array.isArray(moduleFederation.exposes)
98
+ ? moduleFederation.exposes.filter(expose => typeof expose === 'string')
99
+ : [],
100
+ verticalRefs: Array.isArray(moduleFederation.verticalRefs)
101
+ ? moduleFederation.verticalRefs.filter(ref => typeof ref === 'string')
102
+ : [],
103
+ marker: {
104
+ appId: id,
105
+ },
106
+ routes:
107
+ rawApp.routes && typeof rawApp.routes === 'object'
108
+ ? rawApp.routes
109
+ : undefined,
110
+ api:
111
+ rawApp.api && typeof rawApp.api === 'object'
112
+ ? {
113
+ stem:
114
+ typeof rawApp.api.stem === 'string'
115
+ ? rawApp.api.stem
116
+ : domain ?? id,
117
+ prefix:
118
+ typeof rawApp.api.prefix === 'string'
119
+ ? rawApp.api.prefix
120
+ : `/${domain ?? id}-api`,
121
+ }
122
+ : undefined,
123
+ };
124
+ }
125
+
126
+ function createRouteOwnedEntries(app) {
127
+ const namespace = appNamespace(app);
128
+ const base = {
129
+ descriptionKey: `${namespace}.seo.description`,
130
+ mfBoundaryId: app.mfName,
131
+ namespace,
132
+ ownerAppId: app.id,
133
+ public: false,
134
+ indexable: false,
135
+ publicSurface: 'private-app-screen',
136
+ };
137
+
138
+ return [
139
+ {
140
+ ...base,
141
+ canonicalPath: '/',
142
+ id: app.kind === 'shell' ? 'shell-home' : `${app.id}-home`,
143
+ localisedPaths: {
144
+ cs: '/',
145
+ en: '/',
146
+ },
147
+ titleKey: app.kind === 'shell' ? 'shell.title' : `${namespace}.title`,
148
+ },
149
+ ];
150
+ }
151
+
152
+ function createPublicRoutes(app) {
153
+ return createRouteOwnedEntries(app)
154
+ .filter(route => route.public && route.indexable)
155
+ .map(route => ({
156
+ canonicalPath: route.canonicalPath,
157
+ id: route.id,
158
+ localisedPaths: route.localisedPaths,
159
+ namespace: route.namespace,
160
+ ownerAppId: route.ownerAppId,
161
+ descriptionKey: route.descriptionKey,
162
+ titleKey: route.titleKey,
163
+ }));
164
+ }
165
+
166
+ function createPublicSurface(app) {
167
+ const publicRoutes = createPublicRoutes(app);
168
+ const basePublicSurface = {
169
+ authoring: 'colocated-route-meta',
170
+ artifactLifecycle: 'build-and-deploy-output',
171
+ generatedManifest: './src/routes/ultramodern-route-metadata',
172
+ source: 'route-owned-public-routes',
173
+ metadataExport: './src/routes/ultramodern-route-metadata',
174
+ generator: 'scripts/generate-public-surface-assets.mts',
175
+ outputRoot: 'dist/public',
176
+ cloudflareOutputRoot: '.output/public',
177
+ privateRoutePolicy: 'omit-from-generated-public-surface',
178
+ files:
179
+ publicRoutes.length > 0
180
+ ? ['robots.txt', 'sitemap.xml', 'site.webmanifest']
181
+ : ['robots.txt'],
182
+ omittedByDefault: ['api-catalog.json', 'llms.txt', 'security.txt'],
183
+ languages: ['en', 'cs'],
184
+ contentExpansion: {
185
+ authoring: 'route-owned-esm-provider',
186
+ defaultProviderFile: 'route.sitemap.mjs',
187
+ entryExport: 'default-or-entries',
188
+ paramsSource: 'params-or-localeParams',
189
+ draftPolicy: 'omit-draft-by-default',
190
+ indexablePolicy: 'omit-indexable-false',
191
+ lifecycle: 'executed-during-public-surface-generation',
192
+ },
193
+ contentSources: [],
194
+ publicRoutes,
195
+ routeEntries: [],
196
+ concreteUrlPaths: [],
197
+ };
198
+
199
+ return app.routes?.publicSurface &&
200
+ typeof app.routes.publicSurface === 'object'
201
+ ? {
202
+ ...basePublicSurface,
203
+ ...app.routes.publicSurface,
204
+ }
205
+ : basePublicSurface;
206
+ }
207
+
208
+ function createCloudflareDeploy(config, app) {
209
+ const packageScope =
210
+ typeof config.workspace?.packageScope === 'string'
211
+ ? config.workspace.packageScope
212
+ : path.basename(workspaceRoot);
213
+ const compatibilityDate =
214
+ typeof config.deploy?.worker?.compatibilityDate === 'string'
215
+ ? config.deploy.worker.compatibilityDate
216
+ : '2026-06-02';
217
+ return {
218
+ workerName: `${toKebabCase(packageScope)}-${app.packageSuffix}`.slice(0, 63),
219
+ publicUrlEnv: `ULTRAMODERN_PUBLIC_URL_${toEnvSegment(app.id)}`,
220
+ compatibilityDate,
221
+ compatibilityFlags: ['nodejs_compat', 'global_fetch_strictly_public'],
222
+ };
223
+ }
224
+
225
+ function synthesizeContractFromCompactConfig(config) {
226
+ const apps = Array.isArray(config.topology?.apps)
227
+ ? config.topology.apps.map(normalizeCompactApp)
228
+ : [];
229
+
230
+ return {
231
+ sourcePath: compactConfigPath,
232
+ apps: apps.map(app => ({
233
+ ...app,
234
+ deploy: {
235
+ cloudflare: createCloudflareDeploy(config, app),
236
+ },
237
+ routes: {
238
+ publicSurface: createPublicSurface(app),
239
+ },
240
+ })),
241
+ };
242
+ }
243
+
244
+ function readGeneratedContractView() {
245
+ if (fs.existsSync(compactConfigPath)) {
246
+ return synthesizeContractFromCompactConfig(readJson(compactConfigPath));
247
+ }
248
+ throw new Error(
249
+ `Missing UltraModern config. Expected ${path.relative(
250
+ workspaceRoot,
251
+ compactConfigPath,
252
+ )}.`,
253
+ );
254
+ }
255
+
19
256
  function parseArgs(argv) {
20
257
  const parsed = {
21
258
  appId: undefined,
@@ -52,7 +289,7 @@ function parseArgs(argv) {
52
289
 
53
290
  function printHelp() {
54
291
  process.stdout.write(`Usage:
55
- node scripts/generate-public-surface-assets.mjs --app shell-super-app [--target dist|cloudflare] [--require-public-origin]
292
+ node scripts/generate-public-surface-assets.mts --app shell-super-app [--target dist|cloudflare] [--require-public-origin]
56
293
 
57
294
  Set each app's production URL using the contract env key, for example:
58
295
  ULTRAMODERN_PUBLIC_URL_SHELL_SUPER_APP=https://example.com
@@ -517,10 +754,10 @@ try {
517
754
  printHelp();
518
755
  process.exit(0);
519
756
  }
520
- const contract = readJson(contractPath);
757
+ const contract = readGeneratedContractView();
521
758
  const app = contract.apps?.find(candidate => candidate.id === args.appId);
522
759
  if (!app) {
523
- throw new Error(`Unknown app in generated contract: ${args.appId}`);
760
+ throw new Error(`Unknown app in UltraModern config: ${args.appId}`);
524
761
  }
525
762
  await generatePublicSurfaceAssets(app, args.target, args.requirePublicOrigin);
526
763
  } catch (error) {