@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.
- package/README.md +49 -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 +431 -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 +128 -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 +39 -12
- package/dist/cjs/ultramodern-workspace/overlays.cjs +2 -2
- package/dist/cjs/ultramodern-workspace/package-json.cjs +58 -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 +15 -0
- package/dist/cjs/ultramodern-workspace/workspace-scripts.cjs +120 -50
- package/dist/cjs/ultramodern-workspace/write-workspace.cjs +64 -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 +378 -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 +130 -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 +40 -13
- package/dist/esm/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm/ultramodern-workspace/package-json.js +57 -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 +7 -1
- package/dist/esm/ultramodern-workspace/workspace-scripts.js +108 -34
- package/dist/esm/ultramodern-workspace/write-workspace.js +66 -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 +379 -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 +130 -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 +40 -13
- package/dist/esm-node/ultramodern-workspace/overlays.js +2 -2
- package/dist/esm-node/ultramodern-workspace/package-json.js +57 -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 +7 -1
- package/dist/esm-node/ultramodern-workspace/workspace-scripts.js +108 -34
- package/dist/esm-node/ultramodern-workspace/write-workspace.js +66 -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 +6 -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 +55 -13
- 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 +7 -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 +366 -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 +842 -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
|
@@ -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
|
|
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.
|
|
15
|
-
falsy(process.env.
|
|
17
|
+
truthy(process.env.ULTRAMODERN_SKIP_CODEX_SKILLS) ||
|
|
18
|
+
falsy(process.env.ULTRAMODERN_CODEX_SKILLS);
|
|
16
19
|
const cloneTimeoutMs = Number.parseInt(
|
|
17
|
-
process.env.
|
|
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
|
|
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 .
|
|
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 ?? '.
|
|
183
|
+
const installDir = path.join(root, lock.installDir ?? '.codex/skills');
|
|
181
184
|
const sources = lock.sources ?? [];
|
|
182
|
-
const
|
|
183
|
-
|
|
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
|
|
189
|
-
[...
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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 = '
|
|
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
|
|
211
|
-
|
|
212
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
267
|
+
const cloneInstallSources = postinstall
|
|
268
|
+
? cloneSources
|
|
269
|
+
: [...cloneSources, ...optionalCloneSources];
|
|
254
270
|
|
|
255
|
-
for (const source of
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
});
|