@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.
- package/README.md +25 -23
- package/dist/cjs/index.cjs +37 -4
- package/dist/cjs/locale/en.cjs +12 -2
- package/dist/cjs/locale/zh.cjs +12 -2
- package/dist/cjs/ultramodern-tooling/commands.cjs +185 -0
- package/dist/cjs/ultramodern-tooling/config.cjs +172 -0
- package/dist/cjs/ultramodern-workspace/add-vertical.cjs +87 -75
- package/dist/cjs/ultramodern-workspace/{effect-api.cjs → api.cjs} +207 -208
- package/dist/cjs/ultramodern-workspace/bridge-config.cjs +327 -0
- package/dist/cjs/ultramodern-workspace/codesmith.cjs +1 -0
- package/dist/cjs/ultramodern-workspace/contracts.cjs +127 -657
- package/dist/cjs/ultramodern-workspace/demo-components.cjs +12 -12
- package/dist/cjs/ultramodern-workspace/descriptors.cjs +25 -25
- package/dist/cjs/ultramodern-workspace/fs-io.cjs +117 -0
- package/dist/cjs/ultramodern-workspace/generation-result.cjs +5 -5
- package/dist/cjs/ultramodern-workspace/index.cjs +4 -0
- package/dist/cjs/ultramodern-workspace/mf-validation.cjs +571 -0
- package/dist/cjs/ultramodern-workspace/module-federation.cjs +33 -18
- package/dist/cjs/ultramodern-workspace/overlays.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/package-json.cjs +57 -41
- package/dist/cjs/ultramodern-workspace/policy.cjs +4 -4
- package/dist/cjs/ultramodern-workspace/public-api.cjs +4 -0
- package/dist/cjs/ultramodern-workspace/public-surface.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/versions.cjs +5 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +119 -50
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +62 -21
- package/dist/esm/index.js +37 -4
- package/dist/esm/locale/en.js +12 -2
- package/dist/esm/locale/zh.js +12 -2
- package/dist/esm/ultramodern-tooling/commands.js +132 -0
- package/dist/esm/ultramodern-tooling/config.js +120 -0
- package/dist/esm/ultramodern-workspace/add-vertical.js +88 -79
- package/dist/esm/ultramodern-workspace/{effect-api.js → api.js} +146 -147
- package/dist/esm/ultramodern-workspace/bridge-config.js +270 -0
- package/dist/esm/ultramodern-workspace/codesmith.js +1 -0
- package/dist/esm/ultramodern-workspace/contracts.js +129 -611
- package/dist/esm/ultramodern-workspace/demo-components.js +13 -13
- package/dist/esm/ultramodern-workspace/descriptors.js +11 -11
- package/dist/esm/ultramodern-workspace/fs-io.js +116 -2
- package/dist/esm/ultramodern-workspace/generation-result.js +6 -6
- package/dist/esm/ultramodern-workspace/index.js +1 -0
- package/dist/esm/ultramodern-workspace/mf-validation.js +516 -0
- package/dist/esm/ultramodern-workspace/module-federation.js +34 -19
- package/dist/esm/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm/ultramodern-workspace/package-json.js +56 -40
- package/dist/esm/ultramodern-workspace/policy.js +5 -5
- package/dist/esm/ultramodern-workspace/public-api.js +1 -0
- package/dist/esm/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm/ultramodern-workspace/versions.js +3 -1
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +107 -34
- package/dist/esm/ultramodern-workspace/write-workspace.js +64 -26
- package/dist/esm-node/index.js +37 -4
- package/dist/esm-node/locale/en.js +12 -2
- package/dist/esm-node/locale/zh.js +12 -2
- package/dist/esm-node/ultramodern-tooling/commands.js +133 -0
- package/dist/esm-node/ultramodern-tooling/config.js +121 -0
- package/dist/esm-node/ultramodern-workspace/add-vertical.js +88 -79
- package/dist/esm-node/ultramodern-workspace/{effect-api.js → api.js} +146 -147
- package/dist/esm-node/ultramodern-workspace/bridge-config.js +271 -0
- package/dist/esm-node/ultramodern-workspace/codesmith.js +1 -0
- package/dist/esm-node/ultramodern-workspace/contracts.js +129 -611
- package/dist/esm-node/ultramodern-workspace/demo-components.js +13 -13
- package/dist/esm-node/ultramodern-workspace/descriptors.js +11 -11
- package/dist/esm-node/ultramodern-workspace/fs-io.js +116 -2
- package/dist/esm-node/ultramodern-workspace/generation-result.js +6 -6
- package/dist/esm-node/ultramodern-workspace/index.js +1 -0
- package/dist/esm-node/ultramodern-workspace/mf-validation.js +517 -0
- package/dist/esm-node/ultramodern-workspace/module-federation.js +34 -19
- package/dist/esm-node/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm-node/ultramodern-workspace/package-json.js +56 -40
- package/dist/esm-node/ultramodern-workspace/policy.js +5 -5
- package/dist/esm-node/ultramodern-workspace/public-api.js +1 -0
- package/dist/esm-node/ultramodern-workspace/public-surface.js +2 -2
- package/dist/esm-node/ultramodern-workspace/versions.js +3 -1
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +107 -34
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +64 -26
- package/dist/types/locale/en.d.ts +10 -0
- package/dist/types/locale/index.d.ts +20 -0
- package/dist/types/locale/zh.d.ts +10 -0
- package/dist/types/ultramodern-tooling/commands.d.ts +1 -0
- package/dist/types/ultramodern-tooling/config.d.ts +43 -0
- package/dist/types/ultramodern-workspace/add-vertical.d.ts +5 -2
- package/dist/types/ultramodern-workspace/api.d.ts +73 -0
- package/dist/types/ultramodern-workspace/bridge-config.d.ts +64 -0
- package/dist/types/ultramodern-workspace/codesmith.d.ts +1 -0
- package/dist/types/ultramodern-workspace/contracts.d.ts +2 -17
- package/dist/types/ultramodern-workspace/descriptors.d.ts +10 -10
- package/dist/types/ultramodern-workspace/fs-io.d.ts +1 -0
- package/dist/types/ultramodern-workspace/index.d.ts +2 -0
- package/dist/types/ultramodern-workspace/mf-validation.d.ts +27 -0
- package/dist/types/ultramodern-workspace/package-json.d.ts +5 -4
- package/dist/types/ultramodern-workspace/public-api.d.ts +2 -0
- package/dist/types/ultramodern-workspace/types.d.ts +9 -7
- package/dist/types/ultramodern-workspace/versions.d.ts +2 -0
- package/dist/types/ultramodern-workspace/workspace-scripts.d.ts +2 -7
- package/dist/types/ultramodern-workspace/write-workspace.d.ts +4 -2
- package/package.json +5 -3
- package/template-workspace/{.agents → .codex}/skills-lock.json +12 -12
- package/template-workspace/.github/workflows/ultramodern-workspace-gates.yml.handlebars +2 -0
- package/template-workspace/.gitignore.handlebars +3 -0
- package/template-workspace/AGENTS.md.handlebars +11 -5
- package/template-workspace/README.md.handlebars +15 -12
- package/template-workspace/oxfmt.config.ts +1 -0
- package/template-workspace/oxlint.config.ts +1 -0
- package/template-workspace/patches/@tanstack__router-core@1.171.13.patch +51 -0
- package/template-workspace/pnpm-workspace.yaml.handlebars +4 -0
- package/template-workspace/scripts/bootstrap-agent-skills.mjs +77 -67
- package/templates/workspace-scripts/assert-mf-types.mjs.handlebars +51 -8
- package/templates/workspace-scripts/check-ultramodern-api-boundaries.mts +344 -0
- package/templates/workspace-scripts/{check-ultramodern-i18n-boundaries.mjs → check-ultramodern-i18n-boundaries.mts} +1 -1
- package/templates/workspace-scripts/generate-public-surface-assets.mjs +246 -9
- package/templates/workspace-scripts/proof-cloudflare-version.mjs +277 -9
- package/templates/workspace-scripts/ultramodern-cloudflare-proof.mjs +35 -10
- package/templates/workspace-scripts/ultramodern-performance-readiness.mjs +165 -2
- package/templates/workspace-scripts/ultramodern-typecheck.mjs +4 -2
- package/templates/workspace-scripts/validate-ultramodern-workspace.mjs.handlebars +840 -84
- package/dist/types/ultramodern-workspace/effect-api.d.ts +0 -73
- /package/template-workspace/{.agents → .codex}/rstackjs-agent-skills-LICENSE +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsbuild-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/command-map.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/common-analysis-patterns.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-common.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-rspack.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor-webpack.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/install-rsdoctor.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rsdoctor-analysis/references/rsdoctor-data-types.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rslib-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rslib-modern-package/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-best-practices/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/SKILL.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/bottlenecks.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/references/tracing-guide.md +0 -0
- /package/template-workspace/{.agents → .codex}/skills/rspack-tracing/scripts/analyze_trace.js +0 -0
- /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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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) {
|