mfe-platform-cli 1.0.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 +61 -0
- package/index.mjs +1065 -0
- package/package.json +29 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# mfe-platform-cli
|
|
2
|
+
|
|
3
|
+
Генератор воркспейса: host (shell), remote-модули (`services/*`), пакеты `@mfe-platform/shared` и `@mfe-platform/tracing`, манифест `mfe.platform.json`.
|
|
4
|
+
|
|
5
|
+
## Установка
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g mfe-platform-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Или без установки:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx mfe-platform-cli create workspace ./my-app --with-remote shop
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Рекомендуемый сценарий
|
|
18
|
+
|
|
19
|
+
1. **Оболочка + первый микрофронт одной командой** (трассировка уже в `packages/tracing`):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
mfe-platform create workspace ./my-app --with-remote shop --remote-port 4181 --remote-title "Магазин"
|
|
23
|
+
cd my-app && npm install && npm run dev
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. **Второй и следующие микрофронты** (подключаются к тому же host):
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
mfe-platform add remote ./my-app billing --port 4182 --title "Оплата"
|
|
30
|
+
cd my-app && npm install && npm run dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Команды
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
mfe-platform create workspace <targetDir> [--host-name shell] [--host-port 4180] \
|
|
37
|
+
[--with-remote <name> [--remote-port 4181] [--remote-title "Заголовок"]]]
|
|
38
|
+
mfe-platform add remote <workspaceDir> <remoteName> [--port 4181] [--title "Title"]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Локальная разработка (из клонированного репозитория)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install
|
|
45
|
+
npm run platform -- create workspace ./tmp-workspace
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Общее описание монорепозитория и сценарии запуска — в корневом **`README.md`** репозитория `mfe-practice`.
|
|
49
|
+
|
|
50
|
+
## Публикация в npm (для сопровождающих пакет)
|
|
51
|
+
|
|
52
|
+
Публикуйте **из этого каталога**, не из корня `mfe-practice`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
cd tooling/mfe-platform
|
|
56
|
+
npm login
|
|
57
|
+
npm pack --dry-run
|
|
58
|
+
npm publish
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Перед первой публикацией обновите поле `repository.url` в `package.json` под ваш Git-репозиторий.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,1065 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
|
+
|
|
8
|
+
const [, , command, entity, ...argv] = process.argv
|
|
9
|
+
|
|
10
|
+
function fail(message) {
|
|
11
|
+
console.error(`mfe-platform: ${message}`)
|
|
12
|
+
process.exit(1)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function ensureDir(target) {
|
|
16
|
+
fs.mkdirSync(target, { recursive: true })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeFile(target, content) {
|
|
20
|
+
ensureDir(path.dirname(target))
|
|
21
|
+
fs.writeFileSync(target, content, 'utf8')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readJson(target) {
|
|
25
|
+
return JSON.parse(fs.readFileSync(target, 'utf8'))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeJson(target, value) {
|
|
29
|
+
writeFile(target, `${JSON.stringify(value, null, 2)}\n`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(args) {
|
|
33
|
+
const positionals = []
|
|
34
|
+
const options = {}
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
37
|
+
const value = args[i]
|
|
38
|
+
if (!value.startsWith('--')) {
|
|
39
|
+
positionals.push(value)
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const key = value.slice(2)
|
|
44
|
+
const next = args[i + 1]
|
|
45
|
+
if (!next || next.startsWith('--')) {
|
|
46
|
+
options[key] = true
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
options[key] = next
|
|
51
|
+
i += 1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { positionals, options }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toTitle(value) {
|
|
58
|
+
return value
|
|
59
|
+
.split(/[-_\s]+/)
|
|
60
|
+
.filter(Boolean)
|
|
61
|
+
.map((part) => part[0].toUpperCase() + part.slice(1))
|
|
62
|
+
.join(' ')
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function workspacePackageName(workspaceDir) {
|
|
66
|
+
return path.basename(workspaceDir).toLowerCase().replace(/[^a-z0-9-]/g, '-')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function renderWorkspaceManifest({ hostName, hostPort, remotes }) {
|
|
70
|
+
return {
|
|
71
|
+
host: {
|
|
72
|
+
name: hostName,
|
|
73
|
+
title: `${toTitle(hostName)} Host`,
|
|
74
|
+
port: hostPort,
|
|
75
|
+
},
|
|
76
|
+
remotes,
|
|
77
|
+
packages: {
|
|
78
|
+
shared: '@mfe-platform/shared',
|
|
79
|
+
tracing: '@mfe-platform/tracing',
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function renderRootPackageJson(workspaceDir) {
|
|
85
|
+
const resolved = path.resolve(workspaceDir)
|
|
86
|
+
const cliEntry = path.join(__dirname, 'index.mjs')
|
|
87
|
+
const relToCli = path.relative(resolved, cliEntry).split(path.sep).join('/')
|
|
88
|
+
const platformScript = `node ${relToCli}`
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name: workspacePackageName(workspaceDir),
|
|
92
|
+
private: true,
|
|
93
|
+
version: '1.0.0',
|
|
94
|
+
workspaces: ['host', 'services/*', 'packages/*'],
|
|
95
|
+
scripts: {
|
|
96
|
+
dev: 'node ./scripts/dev.mjs',
|
|
97
|
+
build: 'node ./scripts/build.mjs',
|
|
98
|
+
platform: platformScript,
|
|
99
|
+
'mfe:add-remote:example': 'echo "Use the library CLI: npm run platform -- add remote ./TARGET name --port 4181"',
|
|
100
|
+
},
|
|
101
|
+
devDependencies: {
|
|
102
|
+
concurrently: '^9.2.1',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function renderDevScript() {
|
|
108
|
+
return `import { spawn } from 'node:child_process'
|
|
109
|
+
import { readFileSync } from 'node:fs'
|
|
110
|
+
import path from 'node:path'
|
|
111
|
+
import { fileURLToPath } from 'node:url'
|
|
112
|
+
|
|
113
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
114
|
+
const manifest = JSON.parse(readFileSync(path.join(rootDir, 'mfe.platform.json'), 'utf8'))
|
|
115
|
+
|
|
116
|
+
const commands = [
|
|
117
|
+
'npm --prefix host run dev:app',
|
|
118
|
+
...manifest.remotes.map(
|
|
119
|
+
(remote) => \`npm --prefix services/\${remote.name} run dev:remote\`,
|
|
120
|
+
),
|
|
121
|
+
]
|
|
122
|
+
|
|
123
|
+
// Do not use shell: true — the shell would split args and npm --prefix would be parsed as
|
|
124
|
+
// concurrently's own --prefix flag, breaking the logger (prev.replace is not a function).
|
|
125
|
+
const child = spawn('npx', ['concurrently', ...commands], {
|
|
126
|
+
cwd: rootDir,
|
|
127
|
+
stdio: 'inherit',
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
child.on('exit', (code) => process.exit(code ?? 0))
|
|
131
|
+
`
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function renderBuildScript() {
|
|
135
|
+
return `import { execSync } from 'node:child_process'
|
|
136
|
+
import { readFileSync } from 'node:fs'
|
|
137
|
+
import path from 'node:path'
|
|
138
|
+
import { fileURLToPath } from 'node:url'
|
|
139
|
+
|
|
140
|
+
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
141
|
+
const manifest = JSON.parse(readFileSync(path.join(rootDir, 'mfe.platform.json'), 'utf8'))
|
|
142
|
+
|
|
143
|
+
execSync('npm --prefix host run build', { cwd: rootDir, stdio: 'inherit' })
|
|
144
|
+
for (const remote of manifest.remotes) {
|
|
145
|
+
execSync(\`npm --prefix services/\${remote.name} run build\`, { cwd: rootDir, stdio: 'inherit' })
|
|
146
|
+
}
|
|
147
|
+
`
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function renderWorkspaceReadme() {
|
|
151
|
+
return `# MFE Platform Workspace
|
|
152
|
+
|
|
153
|
+
Сгенерированное workspace-приложение для host и набора remote-модулей.
|
|
154
|
+
|
|
155
|
+
## Команды
|
|
156
|
+
|
|
157
|
+
\`npm install\`
|
|
158
|
+
|
|
159
|
+
\`npm run dev\` — из **корня воркспейса** поднять host и все remotes.
|
|
160
|
+
|
|
161
|
+
\`cd host && npm run dev\` — то же самое: скрипт \`dev\` в host запускает общий \`scripts/dev.mjs\` и поднимает Vite-оболочку (\`dev:app\`) и все \`services/*\`.
|
|
162
|
+
|
|
163
|
+
\`npm run build\` — собрать host и все remotes.
|
|
164
|
+
|
|
165
|
+
## Манифест
|
|
166
|
+
|
|
167
|
+
Файл \`mfe.platform.json\` содержит конфигурацию host и список remote-модулей.
|
|
168
|
+
После генерации достаточно выполнить один раз \`npm install\` в корне workspace: npm workspaces установят зависимости для host, services и packages автоматически.
|
|
169
|
+
|
|
170
|
+
Рекомендуемый сценарий: \`mfe-platform create workspace ./app --with-remote shop\`, затем \`cd app && npm install && npm run dev\` — сразу поднимаются оболочка и первый микрофронт. Дальше: \`mfe-platform add remote ./app payments --port 4182\`. Порты по умолчанию (4180+), чтобы не пересекаться с демо в корне репозитория (4170–4172).
|
|
171
|
+
|
|
172
|
+
Host-файлы \`host/src/remotes.generated.ts\`, \`host/src/federation.d.ts\` и \`host/vite.config.ts\` обновляются через CLI.
|
|
173
|
+
`
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function renderTracingPackageJson() {
|
|
177
|
+
return {
|
|
178
|
+
name: '@mfe-platform/tracing',
|
|
179
|
+
private: true,
|
|
180
|
+
version: '1.0.0',
|
|
181
|
+
type: 'module',
|
|
182
|
+
main: './src/index.ts',
|
|
183
|
+
types: './src/index.ts',
|
|
184
|
+
exports: {
|
|
185
|
+
'.': './src/index.ts',
|
|
186
|
+
'./package.json': './package.json',
|
|
187
|
+
},
|
|
188
|
+
dependencies: {
|
|
189
|
+
'@opentelemetry/api': '^1.9.0',
|
|
190
|
+
'@opentelemetry/context-zone': '^2.6.0',
|
|
191
|
+
'@opentelemetry/resources': '^2.1.0',
|
|
192
|
+
'@opentelemetry/sdk-trace-base': '^2.6.0',
|
|
193
|
+
'@opentelemetry/sdk-trace-web': '^2.6.0',
|
|
194
|
+
},
|
|
195
|
+
peerDependencies: {
|
|
196
|
+
react: '>=18',
|
|
197
|
+
'react-dom': '>=18',
|
|
198
|
+
},
|
|
199
|
+
devDependencies: {
|
|
200
|
+
typescript: '~5.9.3',
|
|
201
|
+
},
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function renderTracingTsconfig() {
|
|
206
|
+
return `{
|
|
207
|
+
"compilerOptions": {
|
|
208
|
+
"target": "ES2020",
|
|
209
|
+
"module": "ESNext",
|
|
210
|
+
"moduleResolution": "bundler",
|
|
211
|
+
"strict": true,
|
|
212
|
+
"skipLibCheck": true,
|
|
213
|
+
"noEmit": true
|
|
214
|
+
},
|
|
215
|
+
"include": ["src"]
|
|
216
|
+
}
|
|
217
|
+
`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function renderTracingIndex() {
|
|
221
|
+
return `import { trace, SpanStatusCode } from '@opentelemetry/api'
|
|
222
|
+
import { resourceFromAttributes } from '@opentelemetry/resources'
|
|
223
|
+
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
|
|
224
|
+
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'
|
|
225
|
+
import { ZoneContextManager } from '@opentelemetry/context-zone'
|
|
226
|
+
|
|
227
|
+
export type TraceEntry = {
|
|
228
|
+
timestamp: string
|
|
229
|
+
eventName: string
|
|
230
|
+
traceId: string
|
|
231
|
+
attributes: Record<string, string>
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export type TracingOptions = {
|
|
235
|
+
serviceName?: string
|
|
236
|
+
onRecord?: (entry: TraceEntry) => void
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let initialized = false
|
|
240
|
+
let activeServiceName = 'mfe-host'
|
|
241
|
+
let sink: ((entry: TraceEntry) => void) | undefined
|
|
242
|
+
const traceEntries: TraceEntry[] = []
|
|
243
|
+
const listeners = new Set<(entries: TraceEntry[]) => void>()
|
|
244
|
+
|
|
245
|
+
export function initTracing(options: TracingOptions = {}) {
|
|
246
|
+
if (initialized) return
|
|
247
|
+
|
|
248
|
+
activeServiceName = options.serviceName ?? activeServiceName
|
|
249
|
+
sink = options.onRecord
|
|
250
|
+
|
|
251
|
+
const provider = new WebTracerProvider({
|
|
252
|
+
resource: resourceFromAttributes({ 'service.name': activeServiceName }),
|
|
253
|
+
spanProcessors: [
|
|
254
|
+
new BatchSpanProcessor({
|
|
255
|
+
export(_spans, resultCallback) {
|
|
256
|
+
resultCallback({ code: 0 })
|
|
257
|
+
},
|
|
258
|
+
shutdown() {
|
|
259
|
+
return Promise.resolve()
|
|
260
|
+
},
|
|
261
|
+
}),
|
|
262
|
+
],
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
provider.register({
|
|
266
|
+
contextManager: new ZoneContextManager(),
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
initialized = true
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function recordTraceEvent(eventName: string, attributes: Record<string, string> = {}) {
|
|
273
|
+
try {
|
|
274
|
+
const tracer = trace.getTracer(activeServiceName, '1.0.0')
|
|
275
|
+
const span = tracer.startSpan(eventName, { attributes })
|
|
276
|
+
span.setStatus({ code: SpanStatusCode.OK })
|
|
277
|
+
|
|
278
|
+
const entry: TraceEntry = {
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
eventName,
|
|
281
|
+
traceId: span.spanContext().traceId,
|
|
282
|
+
attributes,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
span.end()
|
|
286
|
+
traceEntries.unshift(entry)
|
|
287
|
+
if (traceEntries.length > 20) traceEntries.pop()
|
|
288
|
+
|
|
289
|
+
listeners.forEach((listener) => listener([...traceEntries]))
|
|
290
|
+
sink?.(entry)
|
|
291
|
+
return entry
|
|
292
|
+
} catch {
|
|
293
|
+
return undefined
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function subscribeTraceEvents(listener: (entries: TraceEntry[]) => void) {
|
|
298
|
+
listeners.add(listener)
|
|
299
|
+
listener([...traceEntries])
|
|
300
|
+
return () => listeners.delete(listener)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function getTraceEvents() {
|
|
304
|
+
return [...traceEntries]
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function getLastTraceIds() {
|
|
308
|
+
return traceEntries.map((entry) => entry.traceId)
|
|
309
|
+
}
|
|
310
|
+
`
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function renderSharedPackageJson() {
|
|
314
|
+
return {
|
|
315
|
+
name: '@mfe-platform/shared',
|
|
316
|
+
private: true,
|
|
317
|
+
version: '1.0.0',
|
|
318
|
+
type: 'module',
|
|
319
|
+
main: './src/index.ts',
|
|
320
|
+
types: './src/index.ts',
|
|
321
|
+
exports: {
|
|
322
|
+
'.': './src/index.ts',
|
|
323
|
+
'./package.json': './package.json',
|
|
324
|
+
},
|
|
325
|
+
dependencies: {
|
|
326
|
+
zustand: '^5.0.2',
|
|
327
|
+
},
|
|
328
|
+
peerDependencies: {
|
|
329
|
+
react: '>=18',
|
|
330
|
+
'react-dom': '>=18',
|
|
331
|
+
},
|
|
332
|
+
devDependencies: {
|
|
333
|
+
typescript: '~5.9.3',
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function renderSharedTsconfig() {
|
|
339
|
+
return renderTracingTsconfig()
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderSharedIndex() {
|
|
343
|
+
return `import { create } from 'zustand'
|
|
344
|
+
|
|
345
|
+
export type ServiceSnapshot = {
|
|
346
|
+
key: string
|
|
347
|
+
displayName: string
|
|
348
|
+
actionCount: number
|
|
349
|
+
lastAction: string
|
|
350
|
+
updatedAt: string
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export type PlatformStore = {
|
|
354
|
+
services: Record<string, ServiceSnapshot>
|
|
355
|
+
registerService: (key: string, displayName: string) => void
|
|
356
|
+
recordAction: (key: string, actionName: string) => void
|
|
357
|
+
resetService: (key: string) => void
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export const usePlatformStore = create<PlatformStore>((set) => ({
|
|
361
|
+
services: {},
|
|
362
|
+
registerService: (key, displayName) =>
|
|
363
|
+
set((state) => ({
|
|
364
|
+
services: {
|
|
365
|
+
...state.services,
|
|
366
|
+
[key]: state.services[key] ?? {
|
|
367
|
+
key,
|
|
368
|
+
displayName,
|
|
369
|
+
actionCount: 0,
|
|
370
|
+
lastAction: 'registered',
|
|
371
|
+
updatedAt: new Date().toISOString(),
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
})),
|
|
375
|
+
recordAction: (key, actionName) =>
|
|
376
|
+
set((state) => {
|
|
377
|
+
const current = state.services[key] ?? {
|
|
378
|
+
key,
|
|
379
|
+
displayName: key,
|
|
380
|
+
actionCount: 0,
|
|
381
|
+
lastAction: 'registered',
|
|
382
|
+
updatedAt: new Date().toISOString(),
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
services: {
|
|
387
|
+
...state.services,
|
|
388
|
+
[key]: {
|
|
389
|
+
...current,
|
|
390
|
+
actionCount: current.actionCount + 1,
|
|
391
|
+
lastAction: actionName,
|
|
392
|
+
updatedAt: new Date().toISOString(),
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
}),
|
|
397
|
+
resetService: (key) =>
|
|
398
|
+
set((state) => ({
|
|
399
|
+
services: {
|
|
400
|
+
...state.services,
|
|
401
|
+
[key]: {
|
|
402
|
+
...(state.services[key] ?? {
|
|
403
|
+
key,
|
|
404
|
+
displayName: key,
|
|
405
|
+
}),
|
|
406
|
+
actionCount: 0,
|
|
407
|
+
lastAction: 'reset',
|
|
408
|
+
updatedAt: new Date().toISOString(),
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
})),
|
|
412
|
+
}))
|
|
413
|
+
`
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function renderHostPackageJson() {
|
|
417
|
+
return {
|
|
418
|
+
name: 'host',
|
|
419
|
+
private: true,
|
|
420
|
+
version: '0.0.0',
|
|
421
|
+
type: 'module',
|
|
422
|
+
scripts: {
|
|
423
|
+
dev: 'node ../scripts/dev.mjs',
|
|
424
|
+
'dev:app': 'vite',
|
|
425
|
+
build: 'vite build',
|
|
426
|
+
preview: 'vite preview',
|
|
427
|
+
},
|
|
428
|
+
dependencies: {
|
|
429
|
+
'@mfe-platform/shared': 'file:../packages/shared',
|
|
430
|
+
'@mfe-platform/tracing': 'file:../packages/tracing',
|
|
431
|
+
'@originjs/vite-plugin-federation': '^1.4.1',
|
|
432
|
+
react: '^19.2.0',
|
|
433
|
+
'react-dom': '^19.2.0',
|
|
434
|
+
zustand: '^5.0.2',
|
|
435
|
+
},
|
|
436
|
+
devDependencies: {
|
|
437
|
+
'@types/react': '^19.2.2',
|
|
438
|
+
'@types/react-dom': '^19.2.2',
|
|
439
|
+
'@vitejs/plugin-react': '^6.0.0',
|
|
440
|
+
typescript: '~5.9.3',
|
|
441
|
+
vite: '^8.0.0',
|
|
442
|
+
},
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function renderHostTsconfig() {
|
|
447
|
+
return `{
|
|
448
|
+
"compilerOptions": {
|
|
449
|
+
"target": "ES2020",
|
|
450
|
+
"module": "ESNext",
|
|
451
|
+
"moduleResolution": "bundler",
|
|
452
|
+
"jsx": "react-jsx",
|
|
453
|
+
"strict": true,
|
|
454
|
+
"skipLibCheck": true,
|
|
455
|
+
"noEmit": true,
|
|
456
|
+
"types": ["vite/client"]
|
|
457
|
+
},
|
|
458
|
+
"include": ["src", "vite.config.ts"]
|
|
459
|
+
}
|
|
460
|
+
`
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function renderHostMain() {
|
|
464
|
+
return `import { StrictMode } from 'react'
|
|
465
|
+
import { createRoot } from 'react-dom/client'
|
|
466
|
+
import { initTracing } from '@mfe-platform/tracing'
|
|
467
|
+
import './index.css'
|
|
468
|
+
import App from './App'
|
|
469
|
+
|
|
470
|
+
initTracing({ serviceName: 'mfe-host' })
|
|
471
|
+
|
|
472
|
+
createRoot(document.getElementById('root')!).render(
|
|
473
|
+
<StrictMode>
|
|
474
|
+
<App />
|
|
475
|
+
</StrictMode>,
|
|
476
|
+
)
|
|
477
|
+
`
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function renderHostApp() {
|
|
481
|
+
return `import { Suspense, useEffect, useMemo, useState } from 'react'
|
|
482
|
+
import { subscribeTraceEvents, type TraceEntry, recordTraceEvent } from '@mfe-platform/tracing'
|
|
483
|
+
import { usePlatformStore } from '@mfe-platform/shared'
|
|
484
|
+
import { remoteModules } from './remotes.generated'
|
|
485
|
+
import './App.css'
|
|
486
|
+
|
|
487
|
+
function App() {
|
|
488
|
+
const [activeKey, setActiveKey] = useState(remoteModules[0]?.key ?? '')
|
|
489
|
+
const [traceEntries, setTraceEntries] = useState<TraceEntry[]>([])
|
|
490
|
+
const servicesRecord = usePlatformStore((state) => state.services)
|
|
491
|
+
const services = useMemo(() => Object.values(servicesRecord), [servicesRecord])
|
|
492
|
+
|
|
493
|
+
useEffect(() => subscribeTraceEvents(setTraceEntries), [])
|
|
494
|
+
|
|
495
|
+
const activeRemote = useMemo(
|
|
496
|
+
() => remoteModules.find((remote) => remote.key === activeKey) ?? remoteModules[0],
|
|
497
|
+
[activeKey],
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const ActiveComponent = activeRemote?.Component
|
|
501
|
+
|
|
502
|
+
const switchTab = (nextKey: string) => {
|
|
503
|
+
setActiveKey(nextKey)
|
|
504
|
+
recordTraceEvent('host:tab-switched', { 'mfe.target': nextKey })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return (
|
|
508
|
+
<main className="shell-layout">
|
|
509
|
+
<header className="shell-header">
|
|
510
|
+
<h1>MFE Platform Host</h1>
|
|
511
|
+
<p>Оболочка подключает удалённые модули и показывает общую наблюдаемость.</p>
|
|
512
|
+
</header>
|
|
513
|
+
|
|
514
|
+
{remoteModules.length > 0 ? (
|
|
515
|
+
<nav className="tabs">
|
|
516
|
+
{remoteModules.map((remote) => (
|
|
517
|
+
<button
|
|
518
|
+
key={remote.key}
|
|
519
|
+
className={remote.key === activeKey ? 'active' : ''}
|
|
520
|
+
onClick={() => switchTab(remote.key)}
|
|
521
|
+
>
|
|
522
|
+
{remote.title}
|
|
523
|
+
</button>
|
|
524
|
+
))}
|
|
525
|
+
</nav>
|
|
526
|
+
) : (
|
|
527
|
+
<section className="empty-state">
|
|
528
|
+
<p>Remote-модули пока не добавлены. Используйте CLI: add remote.</p>
|
|
529
|
+
</section>
|
|
530
|
+
)}
|
|
531
|
+
|
|
532
|
+
<section className="module-container">
|
|
533
|
+
{ActiveComponent ? (
|
|
534
|
+
<Suspense fallback={<p>Загрузка удалённого модуля...</p>}>
|
|
535
|
+
<ActiveComponent />
|
|
536
|
+
</Suspense>
|
|
537
|
+
) : (
|
|
538
|
+
<p>Нет активного remote-модуля.</p>
|
|
539
|
+
)}
|
|
540
|
+
</section>
|
|
541
|
+
|
|
542
|
+
<section className="platform-panels">
|
|
543
|
+
<article className="panel-card">
|
|
544
|
+
<h2>Состояние сервисов</h2>
|
|
545
|
+
{services.length === 0 ? (
|
|
546
|
+
<p>Сервисы зарегистрируются после загрузки remote-модулей.</p>
|
|
547
|
+
) : (
|
|
548
|
+
<ul>
|
|
549
|
+
{services.map((service) => (
|
|
550
|
+
<li key={service.key}>
|
|
551
|
+
<strong>{service.displayName}</strong>
|
|
552
|
+
<span>Действий: {service.actionCount}</span>
|
|
553
|
+
<span>Последнее: {service.lastAction}</span>
|
|
554
|
+
</li>
|
|
555
|
+
))}
|
|
556
|
+
</ul>
|
|
557
|
+
)}
|
|
558
|
+
</article>
|
|
559
|
+
|
|
560
|
+
<article className="panel-card">
|
|
561
|
+
<h2>Trace-события</h2>
|
|
562
|
+
{traceEntries.length === 0 ? (
|
|
563
|
+
<p>Trace-панель обновится после действий в host или remote-модулях.</p>
|
|
564
|
+
) : (
|
|
565
|
+
<ul>
|
|
566
|
+
{traceEntries.slice(0, 8).map((entry) => (
|
|
567
|
+
<li key={entry.timestamp + '-' + entry.eventName}>
|
|
568
|
+
<strong>{entry.eventName}</strong>
|
|
569
|
+
<span>{entry.traceId.slice(0, 16)}...</span>
|
|
570
|
+
</li>
|
|
571
|
+
))}
|
|
572
|
+
</ul>
|
|
573
|
+
)}
|
|
574
|
+
</article>
|
|
575
|
+
</section>
|
|
576
|
+
</main>
|
|
577
|
+
)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export default App
|
|
581
|
+
`
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function renderHostCss() {
|
|
585
|
+
return `.shell-layout {
|
|
586
|
+
max-width: 1120px;
|
|
587
|
+
margin: 0 auto;
|
|
588
|
+
padding: 32px 20px 60px;
|
|
589
|
+
font-family: Inter, Arial, sans-serif;
|
|
590
|
+
color: #132238;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
.shell-header {
|
|
594
|
+
margin-bottom: 24px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.tabs {
|
|
598
|
+
display: flex;
|
|
599
|
+
gap: 12px;
|
|
600
|
+
margin-bottom: 20px;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
.tabs button {
|
|
604
|
+
border: 1px solid #b8c6d9;
|
|
605
|
+
background: #ffffff;
|
|
606
|
+
border-radius: 10px;
|
|
607
|
+
padding: 10px 16px;
|
|
608
|
+
cursor: pointer;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
.tabs button.active {
|
|
612
|
+
background: #1f5eff;
|
|
613
|
+
color: white;
|
|
614
|
+
border-color: #1f5eff;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
.module-container,
|
|
618
|
+
.panel-card,
|
|
619
|
+
.empty-state {
|
|
620
|
+
border: 1px solid #d9e3f0;
|
|
621
|
+
border-radius: 16px;
|
|
622
|
+
background: white;
|
|
623
|
+
padding: 20px;
|
|
624
|
+
box-shadow: 0 10px 32px rgba(17, 38, 68, 0.08);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.platform-panels {
|
|
628
|
+
display: grid;
|
|
629
|
+
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
630
|
+
gap: 16px;
|
|
631
|
+
margin-top: 20px;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
.panel-card ul {
|
|
635
|
+
list-style: none;
|
|
636
|
+
padding: 0;
|
|
637
|
+
margin: 0;
|
|
638
|
+
display: grid;
|
|
639
|
+
gap: 10px;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
.panel-card li {
|
|
643
|
+
display: grid;
|
|
644
|
+
gap: 4px;
|
|
645
|
+
}
|
|
646
|
+
`
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function renderHostIndexCss() {
|
|
650
|
+
return `:root {
|
|
651
|
+
background: #f4f7fb;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
body {
|
|
655
|
+
margin: 0;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
button,
|
|
659
|
+
input {
|
|
660
|
+
font: inherit;
|
|
661
|
+
}
|
|
662
|
+
`
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function renderHostHtml() {
|
|
666
|
+
return `<!doctype html>
|
|
667
|
+
<html lang="ru">
|
|
668
|
+
<head>
|
|
669
|
+
<meta charset="UTF-8" />
|
|
670
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
671
|
+
<title>MFE Platform Host</title>
|
|
672
|
+
</head>
|
|
673
|
+
<body>
|
|
674
|
+
<div id="root"></div>
|
|
675
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
676
|
+
</body>
|
|
677
|
+
</html>
|
|
678
|
+
`
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function renderHostViteConfig(manifest) {
|
|
682
|
+
const remotes = manifest.remotes
|
|
683
|
+
.map((remote) => ` ${remote.name}: 'http://localhost:${remote.port}/assets/remoteEntry.js',`)
|
|
684
|
+
.join('\n')
|
|
685
|
+
|
|
686
|
+
return `import path from 'node:path'
|
|
687
|
+
import { readFileSync } from 'node:fs'
|
|
688
|
+
import { defineConfig } from 'vite'
|
|
689
|
+
import react from '@vitejs/plugin-react'
|
|
690
|
+
import federation from '@originjs/vite-plugin-federation'
|
|
691
|
+
|
|
692
|
+
const manifest = JSON.parse(
|
|
693
|
+
readFileSync(new URL('../mfe.platform.json', import.meta.url), 'utf8'),
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
export default defineConfig({
|
|
697
|
+
resolve: {
|
|
698
|
+
dedupe: ['react', 'react-dom'],
|
|
699
|
+
},
|
|
700
|
+
plugins: [
|
|
701
|
+
react(),
|
|
702
|
+
federation({
|
|
703
|
+
name: manifest.host.name,
|
|
704
|
+
remotes: {
|
|
705
|
+
${remotes}
|
|
706
|
+
},
|
|
707
|
+
shared: ['react', 'react-dom', 'zustand', '@mfe-platform/shared', '@mfe-platform/tracing'],
|
|
708
|
+
}),
|
|
709
|
+
],
|
|
710
|
+
server: {
|
|
711
|
+
port: manifest.host.port,
|
|
712
|
+
strictPort: true,
|
|
713
|
+
fs: {
|
|
714
|
+
allow: [path.resolve(__dirname, '..'), path.resolve(__dirname, '../packages')],
|
|
715
|
+
},
|
|
716
|
+
},
|
|
717
|
+
build: {
|
|
718
|
+
target: 'esnext',
|
|
719
|
+
},
|
|
720
|
+
})
|
|
721
|
+
`
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function renderHostRemotesGenerated(manifest) {
|
|
725
|
+
if (manifest.remotes.length === 0) {
|
|
726
|
+
return `export const remoteModules = [] as Array<{
|
|
727
|
+
key: string
|
|
728
|
+
title: string
|
|
729
|
+
Component: React.ComponentType
|
|
730
|
+
}>
|
|
731
|
+
`
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const imports = [`import { lazy } from 'react'`]
|
|
735
|
+
const entries = manifest.remotes.map((remote) => {
|
|
736
|
+
const alias = `${remote.name}Remote`
|
|
737
|
+
imports.push(`const ${alias} = lazy(() => import('${remote.name}/RemoteApp'))`)
|
|
738
|
+
return ` { key: '${remote.name}', title: '${remote.title}', Component: ${alias} },`
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
return `${imports.join('\n')}
|
|
742
|
+
|
|
743
|
+
export const remoteModules = [
|
|
744
|
+
${entries.join('\n')}
|
|
745
|
+
]
|
|
746
|
+
`
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function renderHostFederationDts(manifest) {
|
|
750
|
+
if (manifest.remotes.length === 0) {
|
|
751
|
+
return `export {}
|
|
752
|
+
`
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return `${manifest.remotes
|
|
756
|
+
.map(
|
|
757
|
+
(remote) => `declare module '${remote.name}/RemoteApp' {
|
|
758
|
+
import { FC } from 'react'
|
|
759
|
+
const Component: FC
|
|
760
|
+
export default Component
|
|
761
|
+
}
|
|
762
|
+
`,
|
|
763
|
+
)
|
|
764
|
+
.join('\n')}`
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function renderRemotePackageJson(name) {
|
|
768
|
+
return {
|
|
769
|
+
name,
|
|
770
|
+
private: true,
|
|
771
|
+
version: '0.0.0',
|
|
772
|
+
type: 'module',
|
|
773
|
+
scripts: {
|
|
774
|
+
dev: 'vite',
|
|
775
|
+
// Стабильная схема: полная сборка + preview. Watch оставляет неполные бандлы и ломает remoteEntry.
|
|
776
|
+
'dev:remote': 'vite build && vite preview --strictPort',
|
|
777
|
+
build: 'vite build',
|
|
778
|
+
preview: 'vite preview',
|
|
779
|
+
},
|
|
780
|
+
dependencies: {
|
|
781
|
+
'@mfe-platform/shared': 'file:../../packages/shared',
|
|
782
|
+
'@mfe-platform/tracing': 'file:../../packages/tracing',
|
|
783
|
+
'@originjs/vite-plugin-federation': '^1.4.1',
|
|
784
|
+
react: '^19.2.0',
|
|
785
|
+
'react-dom': '^19.2.0',
|
|
786
|
+
zustand: '^5.0.2',
|
|
787
|
+
},
|
|
788
|
+
devDependencies: {
|
|
789
|
+
'@types/react': '^19.2.2',
|
|
790
|
+
'@types/react-dom': '^19.2.2',
|
|
791
|
+
'@vitejs/plugin-react': '^6.0.0',
|
|
792
|
+
concurrently: '^9.2.1',
|
|
793
|
+
typescript: '~5.9.3',
|
|
794
|
+
vite: '^8.0.0',
|
|
795
|
+
},
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function renderRemoteTsconfig() {
|
|
800
|
+
return renderHostTsconfig()
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function renderRemoteViteConfig(name, port) {
|
|
804
|
+
return `import { defineConfig } from 'vite'
|
|
805
|
+
import react from '@vitejs/plugin-react'
|
|
806
|
+
import federation from '@originjs/vite-plugin-federation'
|
|
807
|
+
|
|
808
|
+
export default defineConfig({
|
|
809
|
+
resolve: {
|
|
810
|
+
dedupe: ['react', 'react-dom'],
|
|
811
|
+
},
|
|
812
|
+
plugins: [
|
|
813
|
+
react(),
|
|
814
|
+
federation({
|
|
815
|
+
name: '${name}',
|
|
816
|
+
filename: 'remoteEntry.js',
|
|
817
|
+
exposes: {
|
|
818
|
+
'./RemoteApp': './src/RemoteApp.tsx',
|
|
819
|
+
},
|
|
820
|
+
shared: ['react', 'react-dom', 'zustand', '@mfe-platform/shared', '@mfe-platform/tracing'],
|
|
821
|
+
}),
|
|
822
|
+
],
|
|
823
|
+
server: {
|
|
824
|
+
port: ${port},
|
|
825
|
+
strictPort: true,
|
|
826
|
+
},
|
|
827
|
+
preview: {
|
|
828
|
+
port: ${port},
|
|
829
|
+
strictPort: true,
|
|
830
|
+
cors: true,
|
|
831
|
+
},
|
|
832
|
+
build: {
|
|
833
|
+
target: 'esnext',
|
|
834
|
+
minify: false,
|
|
835
|
+
cssCodeSplit: false,
|
|
836
|
+
},
|
|
837
|
+
})
|
|
838
|
+
`
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
function renderRemoteMain() {
|
|
842
|
+
return `import { StrictMode } from 'react'
|
|
843
|
+
import { createRoot } from 'react-dom/client'
|
|
844
|
+
import './index.css'
|
|
845
|
+
import RemoteApp from './RemoteApp'
|
|
846
|
+
|
|
847
|
+
createRoot(document.getElementById('root')!).render(
|
|
848
|
+
<StrictMode>
|
|
849
|
+
<RemoteApp />
|
|
850
|
+
</StrictMode>,
|
|
851
|
+
)
|
|
852
|
+
`
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function renderRemoteHtml(name) {
|
|
856
|
+
return `<!doctype html>
|
|
857
|
+
<html lang="ru">
|
|
858
|
+
<head>
|
|
859
|
+
<meta charset="UTF-8" />
|
|
860
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
861
|
+
<title>${toTitle(name)} Remote</title>
|
|
862
|
+
</head>
|
|
863
|
+
<body>
|
|
864
|
+
<div id="root"></div>
|
|
865
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
866
|
+
</body>
|
|
867
|
+
</html>
|
|
868
|
+
`
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
function renderRemoteIndexCss() {
|
|
872
|
+
return `body {
|
|
873
|
+
margin: 0;
|
|
874
|
+
background: #f4f7fb;
|
|
875
|
+
font-family: Inter, Arial, sans-serif;
|
|
876
|
+
}
|
|
877
|
+
`
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
function renderRemoteCss() {
|
|
881
|
+
return `.remote-card {
|
|
882
|
+
border: 1px solid #d9e3f0;
|
|
883
|
+
border-radius: 16px;
|
|
884
|
+
background: white;
|
|
885
|
+
padding: 20px;
|
|
886
|
+
box-shadow: 0 10px 32px rgba(17, 38, 68, 0.08);
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.remote-card button {
|
|
890
|
+
border: none;
|
|
891
|
+
background: #1f5eff;
|
|
892
|
+
color: white;
|
|
893
|
+
border-radius: 10px;
|
|
894
|
+
padding: 10px 14px;
|
|
895
|
+
cursor: pointer;
|
|
896
|
+
}
|
|
897
|
+
`
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
function renderRemoteApp(name, title) {
|
|
901
|
+
return `import { useEffect } from 'react'
|
|
902
|
+
import { usePlatformStore } from '@mfe-platform/shared'
|
|
903
|
+
import { recordTraceEvent } from '@mfe-platform/tracing'
|
|
904
|
+
import './remote.css'
|
|
905
|
+
|
|
906
|
+
const serviceKey = '${name}'
|
|
907
|
+
const displayName = '${title}'
|
|
908
|
+
|
|
909
|
+
export default function RemoteApp() {
|
|
910
|
+
const registerService = usePlatformStore((state) => state.registerService)
|
|
911
|
+
const recordAction = usePlatformStore((state) => state.recordAction)
|
|
912
|
+
const snapshot = usePlatformStore((state) => state.services[serviceKey])
|
|
913
|
+
|
|
914
|
+
useEffect(() => {
|
|
915
|
+
registerService(serviceKey, displayName)
|
|
916
|
+
recordTraceEvent('${name}:mounted', {
|
|
917
|
+
'mfe.kind': 'remote',
|
|
918
|
+
'mfe.service': serviceKey,
|
|
919
|
+
})
|
|
920
|
+
}, [registerService])
|
|
921
|
+
|
|
922
|
+
const handleAction = () => {
|
|
923
|
+
recordAction(serviceKey, 'business-action')
|
|
924
|
+
recordTraceEvent('${name}:business-action', {
|
|
925
|
+
'mfe.kind': 'remote',
|
|
926
|
+
'mfe.service': serviceKey,
|
|
927
|
+
})
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return (
|
|
931
|
+
<section className="remote-card">
|
|
932
|
+
<h2>{displayName}</h2>
|
|
933
|
+
<p>
|
|
934
|
+
Этот remote-модуль создан через CLI платформы. Он автоматически подключён к host,
|
|
935
|
+
shared store и tracing API.
|
|
936
|
+
</p>
|
|
937
|
+
<p>Количество действий: {snapshot?.actionCount ?? 0}</p>
|
|
938
|
+
<p>Последнее действие: {snapshot?.lastAction ?? 'ещё нет'}</p>
|
|
939
|
+
<button onClick={handleAction}>Выполнить действие</button>
|
|
940
|
+
</section>
|
|
941
|
+
)
|
|
942
|
+
}
|
|
943
|
+
`
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function createWorkspace(targetDir, options) {
|
|
947
|
+
const workspaceDir = path.resolve(targetDir)
|
|
948
|
+
ensureDir(workspaceDir)
|
|
949
|
+
|
|
950
|
+
const hostName = options['host-name'] || 'shell'
|
|
951
|
+
const hostPort = Number(options['host-port'] || 4180)
|
|
952
|
+
const manifest = renderWorkspaceManifest({ hostName, hostPort, remotes: [] })
|
|
953
|
+
|
|
954
|
+
writeJson(path.join(workspaceDir, 'mfe.platform.json'), manifest)
|
|
955
|
+
writeJson(path.join(workspaceDir, 'package.json'), renderRootPackageJson(workspaceDir))
|
|
956
|
+
writeFile(path.join(workspaceDir, 'scripts', 'dev.mjs'), renderDevScript())
|
|
957
|
+
writeFile(path.join(workspaceDir, 'scripts', 'build.mjs'), renderBuildScript())
|
|
958
|
+
writeFile(path.join(workspaceDir, 'README.md'), renderWorkspaceReadme())
|
|
959
|
+
|
|
960
|
+
writeJson(path.join(workspaceDir, 'packages', 'tracing', 'package.json'), renderTracingPackageJson())
|
|
961
|
+
writeFile(path.join(workspaceDir, 'packages', 'tracing', 'tsconfig.json'), renderTracingTsconfig())
|
|
962
|
+
writeFile(path.join(workspaceDir, 'packages', 'tracing', 'src', 'index.ts'), renderTracingIndex())
|
|
963
|
+
|
|
964
|
+
writeJson(path.join(workspaceDir, 'packages', 'shared', 'package.json'), renderSharedPackageJson())
|
|
965
|
+
writeFile(path.join(workspaceDir, 'packages', 'shared', 'tsconfig.json'), renderSharedTsconfig())
|
|
966
|
+
writeFile(path.join(workspaceDir, 'packages', 'shared', 'src', 'index.ts'), renderSharedIndex())
|
|
967
|
+
|
|
968
|
+
writeJson(path.join(workspaceDir, 'host', 'package.json'), renderHostPackageJson())
|
|
969
|
+
writeFile(path.join(workspaceDir, 'host', 'tsconfig.json'), renderHostTsconfig())
|
|
970
|
+
writeFile(path.join(workspaceDir, 'host', 'vite.config.ts'), renderHostViteConfig(manifest))
|
|
971
|
+
writeFile(path.join(workspaceDir, 'host', 'index.html'), renderHostHtml())
|
|
972
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'main.tsx'), renderHostMain())
|
|
973
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'App.tsx'), renderHostApp())
|
|
974
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'App.css'), renderHostCss())
|
|
975
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'index.css'), renderHostIndexCss())
|
|
976
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'remotes.generated.ts'), renderHostRemotesGenerated(manifest))
|
|
977
|
+
writeFile(path.join(workspaceDir, 'host', 'src', 'federation.d.ts'), renderHostFederationDts(manifest))
|
|
978
|
+
|
|
979
|
+
ensureDir(path.join(workspaceDir, 'services'))
|
|
980
|
+
|
|
981
|
+
console.log(`Workspace created at ${workspaceDir}`)
|
|
982
|
+
|
|
983
|
+
const firstRemote = options['with-remote']
|
|
984
|
+
if (firstRemote) {
|
|
985
|
+
const name = String(firstRemote)
|
|
986
|
+
addRemote(workspaceDir, name, {
|
|
987
|
+
port: options['remote-port'],
|
|
988
|
+
title: options['remote-title'] || toTitle(name),
|
|
989
|
+
silent: true,
|
|
990
|
+
})
|
|
991
|
+
console.log('')
|
|
992
|
+
console.log(`Готово: оболочка (host) и первый микрофронт «${name}» (трассировка уже в shared/tracing).`)
|
|
993
|
+
console.log(` cd ${workspaceDir}`)
|
|
994
|
+
console.log(` npm install`)
|
|
995
|
+
console.log(` npm run dev`)
|
|
996
|
+
} else {
|
|
997
|
+
console.log(`Next step: mfe-platform add remote ${workspaceDir} <имя> --port 4181`)
|
|
998
|
+
console.log(` или: mfe-platform create workspace ${targetDir} --with-remote <имя>`)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function addRemote(workspaceDir, remoteName, options) {
|
|
1003
|
+
const rootDir = path.resolve(workspaceDir)
|
|
1004
|
+
const manifestPath = path.join(rootDir, 'mfe.platform.json')
|
|
1005
|
+
if (!fs.existsSync(manifestPath)) fail(`manifest not found: ${manifestPath}`)
|
|
1006
|
+
|
|
1007
|
+
const manifest = readJson(manifestPath)
|
|
1008
|
+
if (manifest.remotes.some((remote) => remote.name === remoteName)) {
|
|
1009
|
+
fail(`remote already exists: ${remoteName}`)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const port = Number(options.port || 4181 + manifest.remotes.length)
|
|
1013
|
+
const title = options.title || toTitle(remoteName)
|
|
1014
|
+
manifest.remotes.push({ name: remoteName, title, port })
|
|
1015
|
+
writeJson(manifestPath, manifest)
|
|
1016
|
+
|
|
1017
|
+
const remoteDir = path.join(rootDir, 'services', remoteName)
|
|
1018
|
+
writeJson(path.join(remoteDir, 'package.json'), renderRemotePackageJson(remoteName))
|
|
1019
|
+
writeFile(path.join(remoteDir, 'tsconfig.json'), renderRemoteTsconfig())
|
|
1020
|
+
writeFile(path.join(remoteDir, 'vite.config.ts'), renderRemoteViteConfig(remoteName, port))
|
|
1021
|
+
writeFile(path.join(remoteDir, 'index.html'), renderRemoteHtml(remoteName))
|
|
1022
|
+
writeFile(path.join(remoteDir, 'src', 'main.tsx'), renderRemoteMain())
|
|
1023
|
+
writeFile(path.join(remoteDir, 'src', 'RemoteApp.tsx'), renderRemoteApp(remoteName, title))
|
|
1024
|
+
writeFile(path.join(remoteDir, 'src', 'remote.css'), renderRemoteCss())
|
|
1025
|
+
writeFile(path.join(remoteDir, 'src', 'index.css'), renderRemoteIndexCss())
|
|
1026
|
+
|
|
1027
|
+
writeFile(path.join(rootDir, 'host', 'vite.config.ts'), renderHostViteConfig(manifest))
|
|
1028
|
+
writeFile(path.join(rootDir, 'host', 'src', 'remotes.generated.ts'), renderHostRemotesGenerated(manifest))
|
|
1029
|
+
writeFile(path.join(rootDir, 'host', 'src', 'federation.d.ts'), renderHostFederationDts(manifest))
|
|
1030
|
+
|
|
1031
|
+
if (!options.silent) {
|
|
1032
|
+
console.log(`Remote '${remoteName}' added to ${rootDir}`)
|
|
1033
|
+
console.log(`Run inside workspace:`)
|
|
1034
|
+
console.log(` cd ${rootDir}`)
|
|
1035
|
+
console.log(` npm install`)
|
|
1036
|
+
console.log(` npm run dev`)
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function printHelp() {
|
|
1041
|
+
console.log(`Usage:
|
|
1042
|
+
mfe-platform create workspace <targetDir> [--host-name shell] [--host-port 4180]
|
|
1043
|
+
[--with-remote <name> [--remote-port 4181] [--remote-title "Заголовок"]]]
|
|
1044
|
+
mfe-platform add remote <workspaceDir> <remoteName> [--port 4181] [--title Catalog]
|
|
1045
|
+
|
|
1046
|
+
Сценарий: create workspace с --with-remote → npm install → npm run dev;
|
|
1047
|
+
затем add remote для следующих микрофронтендов.
|
|
1048
|
+
|
|
1049
|
+
(из репозитория: npm run platform -- …)
|
|
1050
|
+
`)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const { positionals, options } = parseArgs(argv)
|
|
1054
|
+
|
|
1055
|
+
if (command === 'create' && entity === 'workspace') {
|
|
1056
|
+
const [targetDir] = positionals
|
|
1057
|
+
if (!targetDir) fail('targetDir is required')
|
|
1058
|
+
createWorkspace(targetDir, options)
|
|
1059
|
+
} else if (command === 'add' && entity === 'remote') {
|
|
1060
|
+
const [workspaceDir, remoteName] = positionals
|
|
1061
|
+
if (!workspaceDir || !remoteName) fail('workspaceDir and remoteName are required')
|
|
1062
|
+
addRemote(workspaceDir, remoteName, options)
|
|
1063
|
+
} else {
|
|
1064
|
+
printHelp()
|
|
1065
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mfe-platform-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI для генерации воркспейса микрофронтендов (Vite, Module Federation, shared store, tracing)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"mfe",
|
|
10
|
+
"micro-frontend",
|
|
11
|
+
"vite",
|
|
12
|
+
"module-federation",
|
|
13
|
+
"cli",
|
|
14
|
+
"scaffold"
|
|
15
|
+
],
|
|
16
|
+
"bin": {
|
|
17
|
+
"mfe-platform": "index.mjs"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"index.mjs",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18"
|
|
25
|
+
},
|
|
26
|
+
"publishConfig": {
|
|
27
|
+
"access": "public"
|
|
28
|
+
}
|
|
29
|
+
}
|