@multisystemsuite/create-mf-app 1.0.5
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 +249 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1774 -0
- package/package.json +51 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/prompts/index.ts
|
|
26
|
+
var import_prompts = require("@inquirer/prompts");
|
|
27
|
+
async function promptCliConfig() {
|
|
28
|
+
const projectName = await (0, import_prompts.input)({
|
|
29
|
+
message: "Project Name:",
|
|
30
|
+
validate: (value) => /^[a-z0-9-]+$/i.test(value) ? true : "Use letters, numbers, and dashes only."
|
|
31
|
+
});
|
|
32
|
+
const appType = await (0, import_prompts.select)({
|
|
33
|
+
message: "Application Type:",
|
|
34
|
+
choices: [
|
|
35
|
+
{ name: "Parent Container App", value: "parent" },
|
|
36
|
+
{ name: "Child Remote App", value: "child" }
|
|
37
|
+
],
|
|
38
|
+
default: "child"
|
|
39
|
+
});
|
|
40
|
+
const children = appType === "parent" ? await (0, import_prompts.checkbox)({
|
|
41
|
+
message: "Child Applications:",
|
|
42
|
+
choices: [
|
|
43
|
+
{ name: "admin", value: "admin", checked: true },
|
|
44
|
+
{ name: "qc", value: "qc", checked: true },
|
|
45
|
+
{ name: "inventory", value: "inventory", checked: true },
|
|
46
|
+
{ name: "shopfloor", value: "shopfloor", checked: true }
|
|
47
|
+
]
|
|
48
|
+
}) : [];
|
|
49
|
+
const sharedRaw = appType === "parent" ? await (0, import_prompts.input)({
|
|
50
|
+
message: "Shared Applications (comma separated):",
|
|
51
|
+
default: "sharedlib",
|
|
52
|
+
validate: (value) => value.trim().length > 0 ? true : "At least one shared application is required in parent mode."
|
|
53
|
+
}) : "";
|
|
54
|
+
const template = await (0, import_prompts.select)({
|
|
55
|
+
message: "Choose Template:",
|
|
56
|
+
choices: [
|
|
57
|
+
{ name: "React + TypeScript (.tsx)", value: "tsx" },
|
|
58
|
+
{ name: "React + JavaScript (.jsx)", value: "jsx" }
|
|
59
|
+
],
|
|
60
|
+
default: "tsx"
|
|
61
|
+
});
|
|
62
|
+
const port = await (0, import_prompts.input)({
|
|
63
|
+
message: "Port Number:",
|
|
64
|
+
default: "3001",
|
|
65
|
+
validate: (value) => /^\d+$/.test(value) ? true : "Port must be numeric."
|
|
66
|
+
});
|
|
67
|
+
const federation = await (0, import_prompts.confirm)({
|
|
68
|
+
message: "Enable Module Federation:",
|
|
69
|
+
default: true
|
|
70
|
+
});
|
|
71
|
+
const docker = await (0, import_prompts.confirm)({
|
|
72
|
+
message: "Enable Docker:",
|
|
73
|
+
default: true
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
projectName: projectName.trim(),
|
|
77
|
+
port: Number(port),
|
|
78
|
+
docker,
|
|
79
|
+
federation,
|
|
80
|
+
template,
|
|
81
|
+
appType,
|
|
82
|
+
children,
|
|
83
|
+
shared: appType === "parent" ? sharedRaw.split(",").map((item) => item.trim()).filter(Boolean) : []
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/generators/project.ts
|
|
88
|
+
var import_node_path2 = __toESM(require("path"));
|
|
89
|
+
var import_fs_extra2 = __toESM(require("fs-extra"));
|
|
90
|
+
|
|
91
|
+
// src/utils/fs.ts
|
|
92
|
+
var import_node_path = __toESM(require("path"));
|
|
93
|
+
var import_fs_extra = __toESM(require("fs-extra"));
|
|
94
|
+
async function assertTargetDirectoryDoesNotExist(config) {
|
|
95
|
+
const projectRoot = import_node_path.default.join(process.cwd(), config.projectName);
|
|
96
|
+
const exists = await import_fs_extra.default.pathExists(projectRoot);
|
|
97
|
+
if (exists) {
|
|
98
|
+
throw new Error(`Directory already exists: ${config.projectName}`);
|
|
99
|
+
}
|
|
100
|
+
return projectRoot;
|
|
101
|
+
}
|
|
102
|
+
async function writeFile(projectRoot, filePath, content) {
|
|
103
|
+
await import_fs_extra.default.outputFile(import_node_path.default.join(projectRoot, filePath), content);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/templates/react-tsx/index.ts
|
|
107
|
+
function getMuiDependenciesForCurrentNode() {
|
|
108
|
+
const nodeMajor = Number(process.versions.node.split(".")[0] || "0");
|
|
109
|
+
if (nodeMajor >= 20) {
|
|
110
|
+
return {
|
|
111
|
+
"@emotion/react": "^11.14.0",
|
|
112
|
+
"@emotion/styled": "^11.14.0",
|
|
113
|
+
"@mui/material": "^7.2.0",
|
|
114
|
+
"@mui/icons-material": "^7.2.0"
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
"@emotion/react": "^11.13.5",
|
|
119
|
+
"@emotion/styled": "^11.13.5",
|
|
120
|
+
"@mui/material": "^6.4.8",
|
|
121
|
+
"@mui/icons-material": "^6.4.8"
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
var SHARED_DIRS = [
|
|
125
|
+
"public",
|
|
126
|
+
"src/assets",
|
|
127
|
+
"src/components/layout",
|
|
128
|
+
"src/entity",
|
|
129
|
+
"src/hooks",
|
|
130
|
+
"src/layouts",
|
|
131
|
+
"src/module-federation",
|
|
132
|
+
"src/pages/Home",
|
|
133
|
+
"src/routes",
|
|
134
|
+
"src/services",
|
|
135
|
+
"src/store",
|
|
136
|
+
"src/styles",
|
|
137
|
+
"src/utils"
|
|
138
|
+
];
|
|
139
|
+
function getTsxDirectories() {
|
|
140
|
+
return [...SHARED_DIRS, "src/types"];
|
|
141
|
+
}
|
|
142
|
+
function getTsxFiles(config) {
|
|
143
|
+
const { projectName, port, federation } = config;
|
|
144
|
+
const muiDependencies = getMuiDependenciesForCurrentNode();
|
|
145
|
+
return {
|
|
146
|
+
".env": `VITE_PORT=${port}
|
|
147
|
+
VITE_APP_NAME=${projectName}
|
|
148
|
+
VITE_REMOTE_ENTRY=http://localhost:${port}/assets/remoteEntry.js
|
|
149
|
+
`,
|
|
150
|
+
".gitignore": "node_modules\ndist\n.env.local\n*.log\n",
|
|
151
|
+
"index.html": `<!DOCTYPE html>
|
|
152
|
+
<html lang="en">
|
|
153
|
+
<head>
|
|
154
|
+
<meta charset="UTF-8" />
|
|
155
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
156
|
+
<title>${projectName}</title>
|
|
157
|
+
</head>
|
|
158
|
+
<body>
|
|
159
|
+
<div id="root"></div>
|
|
160
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
161
|
+
</body>
|
|
162
|
+
</html>
|
|
163
|
+
`,
|
|
164
|
+
"package.json": `${JSON.stringify(
|
|
165
|
+
{
|
|
166
|
+
name: projectName,
|
|
167
|
+
private: true,
|
|
168
|
+
version: "0.1.0",
|
|
169
|
+
type: "module",
|
|
170
|
+
scripts: {
|
|
171
|
+
dev: "vite",
|
|
172
|
+
build: "tsc -b && vite build",
|
|
173
|
+
watch: "vite build --watch --mode localdev",
|
|
174
|
+
preview: "vite preview",
|
|
175
|
+
"preview:local": "vite preview --mode localdev"
|
|
176
|
+
},
|
|
177
|
+
dependencies: {
|
|
178
|
+
...muiDependencies,
|
|
179
|
+
react: "^19.0.0",
|
|
180
|
+
"react-dom": "^19.0.0",
|
|
181
|
+
"react-router-dom": "^7.0.0"
|
|
182
|
+
},
|
|
183
|
+
devDependencies: {
|
|
184
|
+
"@originjs/vite-plugin-federation": "^1.3.9",
|
|
185
|
+
"@types/node": "^22.13.0",
|
|
186
|
+
"@types/react": "^19.0.0",
|
|
187
|
+
"@types/react-dom": "^19.0.0",
|
|
188
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
189
|
+
typescript: "^5.8.0",
|
|
190
|
+
vite: "^7.0.0"
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
null,
|
|
194
|
+
2
|
|
195
|
+
)}
|
|
196
|
+
`,
|
|
197
|
+
"tsconfig.json": `{
|
|
198
|
+
"compilerOptions": {
|
|
199
|
+
"target": "ES2020",
|
|
200
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
201
|
+
"module": "ESNext",
|
|
202
|
+
"skipLibCheck": true,
|
|
203
|
+
"moduleResolution": "Bundler",
|
|
204
|
+
"resolveJsonModule": true,
|
|
205
|
+
"isolatedModules": true,
|
|
206
|
+
"noEmit": true,
|
|
207
|
+
"jsx": "react-jsx",
|
|
208
|
+
"types": ["node"],
|
|
209
|
+
"strict": true
|
|
210
|
+
},
|
|
211
|
+
"include": ["src", "vite.config.ts"]
|
|
212
|
+
}
|
|
213
|
+
`,
|
|
214
|
+
"vite.config.ts": `import { defineConfig, loadEnv } from "vite";
|
|
215
|
+
import react from "@vitejs/plugin-react";
|
|
216
|
+
import { buildFederation } from "./src/module-federation/federation.config";
|
|
217
|
+
|
|
218
|
+
export default defineConfig(({ mode }) => {
|
|
219
|
+
const env = loadEnv(mode, process.cwd(), "");
|
|
220
|
+
const appPort = Number(env.VITE_PORT || ${port});
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
server: { host: true, port: appPort, strictPort: true },
|
|
224
|
+
preview: { port: appPort },
|
|
225
|
+
plugins: [react()${federation ? ", buildFederation()" : ""}],
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
`,
|
|
229
|
+
"README.md": `# ${projectName}
|
|
230
|
+
|
|
231
|
+
Generated by create-mf-app (React + TypeScript).
|
|
232
|
+
`,
|
|
233
|
+
"src/App.tsx": `import { AppRouter } from "./routes/AppRouter";
|
|
234
|
+
import "./styles/global.css";
|
|
235
|
+
|
|
236
|
+
export default function App() {
|
|
237
|
+
return (
|
|
238
|
+
<div className="app-shell">
|
|
239
|
+
<header className="app-header">
|
|
240
|
+
<h1>${projectName} Microfrontend</h1>
|
|
241
|
+
<p>Module Federation Working</p>
|
|
242
|
+
</header>
|
|
243
|
+
<main className="app-content"><AppRouter /></main>
|
|
244
|
+
<footer className="app-footer">
|
|
245
|
+
<span>Template: TSX</span>
|
|
246
|
+
<span>Port: {import.meta.env.VITE_PORT}</span>
|
|
247
|
+
</footer>
|
|
248
|
+
</div>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
`,
|
|
252
|
+
"src/main.tsx": `import React from "react";
|
|
253
|
+
import ReactDOM from "react-dom/client";
|
|
254
|
+
import { BrowserRouter } from "react-router-dom";
|
|
255
|
+
import App from "./App";
|
|
256
|
+
|
|
257
|
+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
|
258
|
+
<React.StrictMode>
|
|
259
|
+
<BrowserRouter><App /></BrowserRouter>
|
|
260
|
+
</React.StrictMode>,
|
|
261
|
+
);
|
|
262
|
+
`,
|
|
263
|
+
"src/components/layout/Header.tsx": `export function Header() {
|
|
264
|
+
return <div className="section-card"><strong>${projectName} Header</strong></div>;
|
|
265
|
+
}
|
|
266
|
+
`,
|
|
267
|
+
"src/pages/Home/HomePage.tsx": `import { Header } from "../../components/layout/Header";
|
|
268
|
+
|
|
269
|
+
export function HomePage() {
|
|
270
|
+
return (
|
|
271
|
+
<section>
|
|
272
|
+
<Header />
|
|
273
|
+
<div className="section-card">Enterprise TSX template is ready.</div>
|
|
274
|
+
</section>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
`,
|
|
278
|
+
"src/routes/AppRouter.tsx": `import { Navigate, Route, Routes } from "react-router-dom";
|
|
279
|
+
import { HomePage } from "../pages/Home/HomePage";
|
|
280
|
+
|
|
281
|
+
export function AppRouter() {
|
|
282
|
+
return <Routes><Route path="/" element={<HomePage />} /><Route path="*" element={<Navigate to="/" replace />} /></Routes>;
|
|
283
|
+
}
|
|
284
|
+
`,
|
|
285
|
+
"src/styles/global.css": `:root { font-family: Inter, system-ui, sans-serif; color: #f3f4f6; background: #111827; }
|
|
286
|
+
* { box-sizing: border-box; }
|
|
287
|
+
body { margin: 0; }
|
|
288
|
+
.app-shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; }
|
|
289
|
+
.app-header,.app-footer { padding: 1rem; background: #0f172a; border-color: #334155; }
|
|
290
|
+
.app-content { padding: 1rem; }
|
|
291
|
+
.section-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; }
|
|
292
|
+
`,
|
|
293
|
+
"src/module-federation/remotes.ts": `export const remotes = { self: import.meta.env.VITE_REMOTE_ENTRY };
|
|
294
|
+
`,
|
|
295
|
+
"src/module-federation/federation.config.ts": `import federation from "@originjs/vite-plugin-federation";
|
|
296
|
+
|
|
297
|
+
export function buildFederation() {
|
|
298
|
+
return federation({
|
|
299
|
+
name: "${projectName}",
|
|
300
|
+
filename: "remoteEntry.js",
|
|
301
|
+
exposes: { "./App": "./src/App.tsx" },
|
|
302
|
+
remotes: {},
|
|
303
|
+
shared: {
|
|
304
|
+
react: { singleton: true } as any,
|
|
305
|
+
"react-dom": { singleton: true } as any,
|
|
306
|
+
"react-router-dom": { singleton: true } as any,
|
|
307
|
+
"@mui/material": { singleton: true } as any,
|
|
308
|
+
"@mui/icons-material": { singleton: true } as any,
|
|
309
|
+
"@emotion/react": { singleton: true } as any,
|
|
310
|
+
"@emotion/styled": { singleton: true } as any
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
`,
|
|
315
|
+
"src/vite-env.d.ts": '/// <reference types="vite/client" />\n'
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/templates/react-jsx/index.ts
|
|
320
|
+
function getMuiDependenciesForCurrentNode2() {
|
|
321
|
+
const nodeMajor = Number(process.versions.node.split(".")[0] || "0");
|
|
322
|
+
if (nodeMajor >= 20) {
|
|
323
|
+
return {
|
|
324
|
+
"@emotion/react": "^11.14.0",
|
|
325
|
+
"@emotion/styled": "^11.14.0",
|
|
326
|
+
"@mui/material": "^7.2.0",
|
|
327
|
+
"@mui/icons-material": "^7.2.0"
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
return {
|
|
331
|
+
"@emotion/react": "^11.13.5",
|
|
332
|
+
"@emotion/styled": "^11.13.5",
|
|
333
|
+
"@mui/material": "^6.4.8",
|
|
334
|
+
"@mui/icons-material": "^6.4.8"
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
var SHARED_DIRS2 = [
|
|
338
|
+
"public",
|
|
339
|
+
"src/assets",
|
|
340
|
+
"src/components/layout",
|
|
341
|
+
"src/entity",
|
|
342
|
+
"src/hooks",
|
|
343
|
+
"src/layouts",
|
|
344
|
+
"src/module-federation",
|
|
345
|
+
"src/pages/Home",
|
|
346
|
+
"src/routes",
|
|
347
|
+
"src/services",
|
|
348
|
+
"src/store",
|
|
349
|
+
"src/styles",
|
|
350
|
+
"src/utils"
|
|
351
|
+
];
|
|
352
|
+
function getJsxDirectories() {
|
|
353
|
+
return SHARED_DIRS2;
|
|
354
|
+
}
|
|
355
|
+
function getJsxFiles(config) {
|
|
356
|
+
const { projectName, port, federation } = config;
|
|
357
|
+
const muiDependencies = getMuiDependenciesForCurrentNode2();
|
|
358
|
+
return {
|
|
359
|
+
".env": `VITE_PORT=${port}
|
|
360
|
+
VITE_APP_NAME=${projectName}
|
|
361
|
+
VITE_REMOTE_ENTRY=http://localhost:${port}/assets/remoteEntry.js
|
|
362
|
+
`,
|
|
363
|
+
".gitignore": "node_modules\ndist\n.env.local\n*.log\n",
|
|
364
|
+
"index.html": `<!DOCTYPE html>
|
|
365
|
+
<html lang="en">
|
|
366
|
+
<head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>${projectName}</title></head>
|
|
367
|
+
<body>
|
|
368
|
+
<div id="root"></div>
|
|
369
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
370
|
+
</body>
|
|
371
|
+
</html>
|
|
372
|
+
`,
|
|
373
|
+
"package.json": `${JSON.stringify(
|
|
374
|
+
{
|
|
375
|
+
name: projectName,
|
|
376
|
+
private: true,
|
|
377
|
+
version: "0.1.0",
|
|
378
|
+
type: "module",
|
|
379
|
+
scripts: {
|
|
380
|
+
dev: "vite",
|
|
381
|
+
build: "vite build",
|
|
382
|
+
watch: "vite build --watch --mode localdev",
|
|
383
|
+
preview: "vite preview",
|
|
384
|
+
"preview:local": "vite preview --mode localdev"
|
|
385
|
+
},
|
|
386
|
+
dependencies: {
|
|
387
|
+
...muiDependencies,
|
|
388
|
+
react: "^19.0.0",
|
|
389
|
+
"react-dom": "^19.0.0",
|
|
390
|
+
"react-router-dom": "^7.0.0"
|
|
391
|
+
},
|
|
392
|
+
devDependencies: {
|
|
393
|
+
"@originjs/vite-plugin-federation": "^1.3.9",
|
|
394
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
395
|
+
vite: "^7.0.0"
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
null,
|
|
399
|
+
2
|
|
400
|
+
)}
|
|
401
|
+
`,
|
|
402
|
+
"jsconfig.json": `{
|
|
403
|
+
"compilerOptions": {
|
|
404
|
+
"target": "ES2020",
|
|
405
|
+
"module": "ESNext",
|
|
406
|
+
"moduleResolution": "Bundler",
|
|
407
|
+
"jsx": "react-jsx"
|
|
408
|
+
},
|
|
409
|
+
"include": ["src", "vite.config.js"]
|
|
410
|
+
}
|
|
411
|
+
`,
|
|
412
|
+
"vite.config.js": `import { defineConfig, loadEnv } from "vite";
|
|
413
|
+
import react from "@vitejs/plugin-react";
|
|
414
|
+
import { buildFederation } from "./src/module-federation/federation.config.js";
|
|
415
|
+
|
|
416
|
+
export default defineConfig(({ mode }) => {
|
|
417
|
+
const env = loadEnv(mode, process.cwd(), "");
|
|
418
|
+
const appPort = Number(env.VITE_PORT || ${port});
|
|
419
|
+
return {
|
|
420
|
+
server: { host: true, port: appPort, strictPort: true },
|
|
421
|
+
preview: { port: appPort },
|
|
422
|
+
plugins: [react()${federation ? ", buildFederation()" : ""}],
|
|
423
|
+
};
|
|
424
|
+
});
|
|
425
|
+
`,
|
|
426
|
+
"README.md": `# ${projectName}
|
|
427
|
+
|
|
428
|
+
Generated by create-mf-app (React + JavaScript).
|
|
429
|
+
`,
|
|
430
|
+
"src/App.jsx": `import { AppRouter } from "./routes/AppRouter";
|
|
431
|
+
import "./styles/global.css";
|
|
432
|
+
|
|
433
|
+
export default function App() {
|
|
434
|
+
return (
|
|
435
|
+
<div className="app-shell">
|
|
436
|
+
<header className="app-header">
|
|
437
|
+
<h1>${projectName} Microfrontend</h1>
|
|
438
|
+
<p>Module Federation Working</p>
|
|
439
|
+
</header>
|
|
440
|
+
<main className="app-content"><AppRouter /></main>
|
|
441
|
+
<footer className="app-footer">
|
|
442
|
+
<span>Template: JSX</span>
|
|
443
|
+
<span>Port: {import.meta.env.VITE_PORT}</span>
|
|
444
|
+
</footer>
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
`,
|
|
449
|
+
"src/main.jsx": `import React from "react";
|
|
450
|
+
import ReactDOM from "react-dom/client";
|
|
451
|
+
import { BrowserRouter } from "react-router-dom";
|
|
452
|
+
import App from "./App";
|
|
453
|
+
|
|
454
|
+
ReactDOM.createRoot(document.getElementById("root")).render(
|
|
455
|
+
<React.StrictMode><BrowserRouter><App /></BrowserRouter></React.StrictMode>,
|
|
456
|
+
);
|
|
457
|
+
`,
|
|
458
|
+
"src/components/layout/Header.jsx": `export function Header() {
|
|
459
|
+
return <div className="section-card"><strong>${projectName} Header</strong></div>;
|
|
460
|
+
}
|
|
461
|
+
`,
|
|
462
|
+
"src/pages/Home/HomePage.jsx": `import { Header } from "../../components/layout/Header";
|
|
463
|
+
|
|
464
|
+
export function HomePage() {
|
|
465
|
+
return (
|
|
466
|
+
<section>
|
|
467
|
+
<Header />
|
|
468
|
+
<div className="section-card">Enterprise JSX template is ready.</div>
|
|
469
|
+
</section>
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
`,
|
|
473
|
+
"src/routes/AppRouter.jsx": `import { Navigate, Route, Routes } from "react-router-dom";
|
|
474
|
+
import { HomePage } from "../pages/Home/HomePage";
|
|
475
|
+
|
|
476
|
+
export function AppRouter() {
|
|
477
|
+
return <Routes><Route path="/" element={<HomePage />} /><Route path="*" element={<Navigate to="/" replace />} /></Routes>;
|
|
478
|
+
}
|
|
479
|
+
`,
|
|
480
|
+
"src/styles/global.css": `:root { font-family: Inter, system-ui, sans-serif; color: #f3f4f6; background: #111827; }
|
|
481
|
+
* { box-sizing: border-box; }
|
|
482
|
+
body { margin: 0; }
|
|
483
|
+
.app-shell { min-height: 100vh; display: grid; grid-template-rows: auto 1fr auto; }
|
|
484
|
+
.app-header,.app-footer { padding: 1rem; background: #0f172a; border-color: #334155; }
|
|
485
|
+
.app-content { padding: 1rem; }
|
|
486
|
+
.section-card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 1rem; }
|
|
487
|
+
`,
|
|
488
|
+
"src/module-federation/remotes.js": `export const remotes = { self: import.meta.env.VITE_REMOTE_ENTRY };
|
|
489
|
+
`,
|
|
490
|
+
"src/module-federation/federation.config.js": `import federation from "@originjs/vite-plugin-federation";
|
|
491
|
+
|
|
492
|
+
export function buildFederation() {
|
|
493
|
+
return federation({
|
|
494
|
+
name: "${projectName}",
|
|
495
|
+
filename: "remoteEntry.js",
|
|
496
|
+
exposes: { "./App": "./src/App.jsx" },
|
|
497
|
+
remotes: {},
|
|
498
|
+
shared: {
|
|
499
|
+
react: { singleton: true },
|
|
500
|
+
"react-dom": { singleton: true },
|
|
501
|
+
"react-router-dom": { singleton: true },
|
|
502
|
+
"@mui/material": { singleton: true },
|
|
503
|
+
"@mui/icons-material": { singleton: true },
|
|
504
|
+
"@emotion/react": { singleton: true },
|
|
505
|
+
"@emotion/styled": { singleton: true }
|
|
506
|
+
},
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
`
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// src/generators/project.ts
|
|
514
|
+
function getDockerfile(port) {
|
|
515
|
+
return `FROM node:22-alpine
|
|
516
|
+
|
|
517
|
+
WORKDIR /app
|
|
518
|
+
COPY . .
|
|
519
|
+
RUN npm install
|
|
520
|
+
EXPOSE ${port}
|
|
521
|
+
CMD ["npm","run","dev","--","--host"]
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
async function generateProject(config) {
|
|
525
|
+
const projectRoot = import_node_path2.default.join(process.cwd(), config.projectName);
|
|
526
|
+
await import_fs_extra2.default.ensureDir(projectRoot);
|
|
527
|
+
if (config.appType === "parent") {
|
|
528
|
+
await generateParentWorkspace(projectRoot, config);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
await generateSingleApp(projectRoot, config.projectName, config.port, config);
|
|
532
|
+
}
|
|
533
|
+
async function generateSingleApp(projectRoot, appName, port, config) {
|
|
534
|
+
const appConfig = { ...config, projectName: appName, port };
|
|
535
|
+
const directories = appConfig.template === "tsx" ? getTsxDirectories() : getJsxDirectories();
|
|
536
|
+
await Promise.all(directories.map((directory) => import_fs_extra2.default.ensureDir(import_node_path2.default.join(projectRoot, directory))));
|
|
537
|
+
const files = appConfig.template === "tsx" ? getTsxFiles(appConfig) : getJsxFiles(appConfig);
|
|
538
|
+
await Promise.all(Object.entries(files).map(([filePath, content]) => writeFile(projectRoot, filePath, content)));
|
|
539
|
+
if (config.template === "tsx") {
|
|
540
|
+
await writeFile(projectRoot, "src/App.tsx", getAppView(appName, port));
|
|
541
|
+
} else {
|
|
542
|
+
await writeFile(projectRoot, "src/App.jsx", getAppView(appName, port));
|
|
543
|
+
}
|
|
544
|
+
if (config.docker) {
|
|
545
|
+
await writeFile(projectRoot, "Dockerfile", getDockerfile(port));
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function generateParentWorkspace(projectRoot, config) {
|
|
549
|
+
const uniqueChildren = Array.from(new Set(config.children));
|
|
550
|
+
const sharedApps = Array.from(new Set(config.shared.length ? config.shared : ["sharedlib"]));
|
|
551
|
+
const mainApp = "main";
|
|
552
|
+
const allApps = [mainApp, ...uniqueChildren, ...sharedApps];
|
|
553
|
+
const remotes = allApps.filter((name) => name !== mainApp);
|
|
554
|
+
const portMap = buildPortMap(allApps, 3e3);
|
|
555
|
+
await Promise.all(["apps", "scripts"].map((directory) => import_fs_extra2.default.ensureDir(import_node_path2.default.join(projectRoot, directory))));
|
|
556
|
+
await Promise.all(["shared-ui", "shared-utils", "shared-auth"].map((pkg) => import_fs_extra2.default.ensureDir(import_node_path2.default.join(projectRoot, `packages/${pkg}`))));
|
|
557
|
+
await Promise.all(
|
|
558
|
+
allApps.map((appName) => generateSingleApp(import_node_path2.default.join(projectRoot, `apps/${appName}`), appName, portMap[appName], config))
|
|
559
|
+
);
|
|
560
|
+
if (config.federation) {
|
|
561
|
+
const remotesMap = Object.fromEntries(remotes.map((name) => [name, portMap[name]]));
|
|
562
|
+
if (config.template === "tsx") {
|
|
563
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/module-federation/federation.config.ts", getHostFederationTs(remotesMap));
|
|
564
|
+
} else {
|
|
565
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/module-federation/federation.config.js", getHostFederationJs(remotesMap));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
await wireSharedLibAndMainNavigation(projectRoot, config, uniqueChildren, sharedApps, portMap);
|
|
569
|
+
await writeFile(projectRoot, "package.json", getWorkspacePackageJson(config.projectName, allApps, remotes));
|
|
570
|
+
await writeFile(projectRoot, ".npmrc", "install-strategy=nested\n");
|
|
571
|
+
await writeFile(projectRoot, "README.md", getWorkspaceReadme(config.projectName, portMap));
|
|
572
|
+
await writeFile(projectRoot, ".env", getRootEnvFile(config.projectName, portMap));
|
|
573
|
+
await writeFile(projectRoot, "docker-compose.yml", getDockerCompose(allApps, portMap, config.docker));
|
|
574
|
+
await writeFile(projectRoot, "print-env.js", getPrintEnvScript());
|
|
575
|
+
await writeFile(projectRoot, "smart-build.js", getSmartBuildScript());
|
|
576
|
+
await writeFile(projectRoot, "live-reload.js", getLiveReloadScript());
|
|
577
|
+
await writeFile(projectRoot, "scripts/bump-version.js", getBumpVersionScript());
|
|
578
|
+
}
|
|
579
|
+
async function wireSharedLibAndMainNavigation(projectRoot, config, children, sharedApps, portMap) {
|
|
580
|
+
const sharedName = sharedApps[0];
|
|
581
|
+
if (!sharedName) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (config.template === "tsx") {
|
|
585
|
+
await writeFile(import_node_path2.default.join(projectRoot, `apps/${sharedName}`), "src/components/layout/SharedBadge.tsx", getSharedBadgeTsx());
|
|
586
|
+
await Promise.all(
|
|
587
|
+
Object.entries(getSharedlibFoundationFilesTs()).map(
|
|
588
|
+
([filePath, content]) => writeFile(import_node_path2.default.join(projectRoot, `apps/${sharedName}`), filePath, content)
|
|
589
|
+
)
|
|
590
|
+
);
|
|
591
|
+
await writeFile(
|
|
592
|
+
import_node_path2.default.join(projectRoot, `apps/${sharedName}`),
|
|
593
|
+
"src/module-federation/federation.config.ts",
|
|
594
|
+
getSharedlibFederationTs(sharedName)
|
|
595
|
+
);
|
|
596
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/App.tsx", getMainNavbarAppTsx(children, sharedName));
|
|
597
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/routes/AppRouter.tsx", getMainRouterTsx(children));
|
|
598
|
+
await Promise.all(
|
|
599
|
+
children.map(
|
|
600
|
+
(child) => writeFile(import_node_path2.default.join(projectRoot, `apps/${child}`), "src/App.tsx", getChildAppTsx(child, portMap[child], sharedName))
|
|
601
|
+
)
|
|
602
|
+
);
|
|
603
|
+
await writeFile(
|
|
604
|
+
import_node_path2.default.join(projectRoot, "apps/main"),
|
|
605
|
+
"src/types/federation-remotes.d.ts",
|
|
606
|
+
getMainFederationTypeDeclTs(children, sharedName)
|
|
607
|
+
);
|
|
608
|
+
await Promise.all(
|
|
609
|
+
children.map(
|
|
610
|
+
(child) => writeFile(
|
|
611
|
+
import_node_path2.default.join(projectRoot, `apps/${child}`),
|
|
612
|
+
"src/types/federation-remotes.d.ts",
|
|
613
|
+
getChildFederationTypeDeclTs(sharedName)
|
|
614
|
+
)
|
|
615
|
+
)
|
|
616
|
+
);
|
|
617
|
+
await Promise.all(
|
|
618
|
+
["main", ...children].map(
|
|
619
|
+
(app) => writeFile(
|
|
620
|
+
import_node_path2.default.join(projectRoot, `apps/${app}`),
|
|
621
|
+
"src/module-federation/federation.config.ts",
|
|
622
|
+
app === "main" ? getHostFederationTs(Object.fromEntries([...children, ...sharedApps].map((r) => [r, portMap[r]]))) : getChildFederationTs(app, sharedName, portMap[sharedName])
|
|
623
|
+
)
|
|
624
|
+
)
|
|
625
|
+
);
|
|
626
|
+
} else {
|
|
627
|
+
await writeFile(import_node_path2.default.join(projectRoot, `apps/${sharedName}`), "src/components/layout/SharedBadge.jsx", getSharedBadgeJsx());
|
|
628
|
+
await writeFile(
|
|
629
|
+
import_node_path2.default.join(projectRoot, `apps/${sharedName}`),
|
|
630
|
+
"src/module-federation/federation.config.js",
|
|
631
|
+
getSharedlibFederationJs(sharedName)
|
|
632
|
+
);
|
|
633
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/App.jsx", getMainNavbarAppJsx(children, sharedName));
|
|
634
|
+
await writeFile(import_node_path2.default.join(projectRoot, "apps/main"), "src/routes/AppRouter.jsx", getMainRouterJsx(children));
|
|
635
|
+
await Promise.all(
|
|
636
|
+
children.map(
|
|
637
|
+
(child) => writeFile(import_node_path2.default.join(projectRoot, `apps/${child}`), "src/App.jsx", getChildAppJsx(child, portMap[child], sharedName))
|
|
638
|
+
)
|
|
639
|
+
);
|
|
640
|
+
await Promise.all(
|
|
641
|
+
["main", ...children].map(
|
|
642
|
+
(app) => writeFile(
|
|
643
|
+
import_node_path2.default.join(projectRoot, `apps/${app}`),
|
|
644
|
+
"src/module-federation/federation.config.js",
|
|
645
|
+
app === "main" ? getHostFederationJs(Object.fromEntries([...children, ...sharedApps].map((r) => [r, portMap[r]]))) : getChildFederationJs(app, sharedName, portMap[sharedName])
|
|
646
|
+
)
|
|
647
|
+
)
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function buildPortMap(apps, startPort) {
|
|
652
|
+
const portMap = {};
|
|
653
|
+
apps.forEach((app, index) => {
|
|
654
|
+
portMap[app] = startPort + index;
|
|
655
|
+
});
|
|
656
|
+
return portMap;
|
|
657
|
+
}
|
|
658
|
+
function getAppView(appName, port) {
|
|
659
|
+
const appLabel = appName.charAt(0).toUpperCase() + appName.slice(1);
|
|
660
|
+
return `import { AppRouter } from "./routes/AppRouter";
|
|
661
|
+
import "./styles/global.css";
|
|
662
|
+
|
|
663
|
+
export default function App() {
|
|
664
|
+
return (
|
|
665
|
+
<div className="app-shell">
|
|
666
|
+
<header className="app-header">
|
|
667
|
+
<h1>${appLabel} Microfrontend</h1>
|
|
668
|
+
<p>Module Federation Working</p>
|
|
669
|
+
</header>
|
|
670
|
+
<main className="app-content"><AppRouter /></main>
|
|
671
|
+
<footer className="app-footer">
|
|
672
|
+
<span>Environment: {import.meta.env.DEV ? "Development" : "Production"}</span>
|
|
673
|
+
<span>Port: ${port}</span>
|
|
674
|
+
</footer>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
680
|
+
function getMainNavbarAppTsx(children, sharedName) {
|
|
681
|
+
const modules = JSON.stringify(children);
|
|
682
|
+
return `import React from "react";
|
|
683
|
+
import { Link } from "react-router-dom";
|
|
684
|
+
import { AppRouter } from "./routes/AppRouter";
|
|
685
|
+
import "./styles/global.css";
|
|
686
|
+
const AppConfigProvider = React.lazy(() => import(${JSON.stringify(`${sharedName}/AppConfigProvider`)}));
|
|
687
|
+
const AppNavbar = React.lazy(() => import(${JSON.stringify(`${sharedName}/AppNavbar`)}));
|
|
688
|
+
|
|
689
|
+
export default function App() {
|
|
690
|
+
const modules = ${modules};
|
|
691
|
+
return (
|
|
692
|
+
<React.Suspense fallback={<div>Loading configuration provider...</div>}>
|
|
693
|
+
<AppConfigProvider>
|
|
694
|
+
<div className="app-shell">
|
|
695
|
+
<header className="app-header">
|
|
696
|
+
<h1>Main Microfrontend</h1>
|
|
697
|
+
<p>Module Federation Working</p>
|
|
698
|
+
<React.Suspense fallback={<div>Loading navbar...</div>}>
|
|
699
|
+
<AppNavbar title="Enterprise Platform">
|
|
700
|
+
<Link to="/">home</Link>
|
|
701
|
+
{" | "}
|
|
702
|
+
{modules.map((name) => (
|
|
703
|
+
<span key={name}>
|
|
704
|
+
<Link to={\`/\${name}\`}>{name}</Link>{" | "}
|
|
705
|
+
</span>
|
|
706
|
+
))}
|
|
707
|
+
<Link to="/settings/theme">theme</Link>
|
|
708
|
+
</AppNavbar>
|
|
709
|
+
</React.Suspense>
|
|
710
|
+
</header>
|
|
711
|
+
<main className="app-content"><AppRouter /></main>
|
|
712
|
+
<footer className="app-footer"><span>Shared: ${sharedName}</span><span>Port: {import.meta.env.VITE_PORT}</span></footer>
|
|
713
|
+
</div>
|
|
714
|
+
</AppConfigProvider>
|
|
715
|
+
</React.Suspense>
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
`;
|
|
719
|
+
}
|
|
720
|
+
function getMainRouterTsx(children) {
|
|
721
|
+
const lazyImports = children.map(
|
|
722
|
+
(child) => `const ${toPascal(child)}Remote = React.lazy(() => import(${JSON.stringify(`${child}/App`)}));`
|
|
723
|
+
).join("\n");
|
|
724
|
+
const routes = children.map((child) => `<Route path="/${child}/*" element={<React.Suspense fallback={<div>Loading ${child}...</div>}><${toPascal(child)}Remote /></React.Suspense>} />`).join("\n ");
|
|
725
|
+
return `import React from "react";
|
|
726
|
+
import { Navigate, Route, Routes } from "react-router-dom";
|
|
727
|
+
const SettingsPage = React.lazy(() => import("sharedlib/SettingsPage"));
|
|
728
|
+
|
|
729
|
+
${lazyImports}
|
|
730
|
+
|
|
731
|
+
export function AppRouter() {
|
|
732
|
+
return (
|
|
733
|
+
<Routes>
|
|
734
|
+
<Route path="/" element={<div>Select a module from navbar.</div>} />
|
|
735
|
+
<Route path="/settings/theme" element={<React.Suspense fallback={<div>Loading settings...</div>}><SettingsPage section="theme" /></React.Suspense>} />
|
|
736
|
+
<Route path="/settings/layout" element={<React.Suspense fallback={<div>Loading settings...</div>}><SettingsPage section="layout" /></React.Suspense>} />
|
|
737
|
+
<Route path="/settings/branding" element={<React.Suspense fallback={<div>Loading settings...</div>}><SettingsPage section="branding" /></React.Suspense>} />
|
|
738
|
+
${routes}
|
|
739
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
740
|
+
</Routes>
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
`;
|
|
744
|
+
}
|
|
745
|
+
function getChildAppTsx(appName, port, sharedName) {
|
|
746
|
+
return `import React from "react";
|
|
747
|
+
import { AppRouter } from "./routes/AppRouter";
|
|
748
|
+
import "./styles/global.css";
|
|
749
|
+
const SharedBadge = React.lazy(() => import(${JSON.stringify(`${sharedName}/SharedBadge`)}));
|
|
750
|
+
|
|
751
|
+
export default function App() {
|
|
752
|
+
return (
|
|
753
|
+
<div className="app-shell">
|
|
754
|
+
<header className="app-header">
|
|
755
|
+
<h1>${toPascal(appName)} Microfrontend</h1>
|
|
756
|
+
<p>Module Federation Working</p>
|
|
757
|
+
</header>
|
|
758
|
+
<main className="app-content">
|
|
759
|
+
<React.Suspense fallback={<div>Loading shared library...</div>}><SharedBadge /></React.Suspense>
|
|
760
|
+
<AppRouter />
|
|
761
|
+
</main>
|
|
762
|
+
<footer className="app-footer"><span>Port: ${port}</span></footer>
|
|
763
|
+
</div>
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
`;
|
|
767
|
+
}
|
|
768
|
+
function getChildFederationTs(appName, sharedName, sharedPort) {
|
|
769
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
770
|
+
|
|
771
|
+
export function buildFederation() {
|
|
772
|
+
return federation({
|
|
773
|
+
name: ${JSON.stringify(appName)},
|
|
774
|
+
filename: "remoteEntry.js",
|
|
775
|
+
exposes: { "./App": "./src/App.tsx" },
|
|
776
|
+
remotes: { ${JSON.stringify(sharedName)}: "http://localhost:${sharedPort}/assets/remoteEntry.js" },
|
|
777
|
+
shared: {
|
|
778
|
+
react: { singleton: true } as any,
|
|
779
|
+
"react-dom": { singleton: true } as any,
|
|
780
|
+
"react-router-dom": { singleton: true } as any,
|
|
781
|
+
"@mui/material": { singleton: true } as any,
|
|
782
|
+
"@mui/icons-material": { singleton: true } as any,
|
|
783
|
+
"@emotion/react": { singleton: true } as any,
|
|
784
|
+
"@emotion/styled": { singleton: true } as any
|
|
785
|
+
},
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
`;
|
|
789
|
+
}
|
|
790
|
+
function getSharedlibFederationTs(sharedName) {
|
|
791
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
792
|
+
|
|
793
|
+
export function buildFederation() {
|
|
794
|
+
return federation({
|
|
795
|
+
name: ${JSON.stringify(sharedName)},
|
|
796
|
+
filename: "remoteEntry.js",
|
|
797
|
+
exposes: {
|
|
798
|
+
"./App": "./src/App.tsx",
|
|
799
|
+
"./SharedBadge": "./src/components/layout/SharedBadge.tsx",
|
|
800
|
+
"./AppConfigProvider": "./src/config/AppConfigProvider.tsx",
|
|
801
|
+
"./useAppConfig": "./src/config/useAppConfig.ts",
|
|
802
|
+
"./SettingsPage": "./src/pages/Settings/SettingsPage.tsx",
|
|
803
|
+
"./AppNavbar": "./src/shared-ui/AppNavbar/AppNavbar.tsx",
|
|
804
|
+
"./AppSidebar": "./src/shared-ui/AppSidebar/AppSidebar.tsx"
|
|
805
|
+
},
|
|
806
|
+
shared: {
|
|
807
|
+
react: { singleton: true } as any,
|
|
808
|
+
"react-dom": { singleton: true } as any,
|
|
809
|
+
"react-router-dom": { singleton: true } as any,
|
|
810
|
+
"@mui/material": { singleton: true } as any,
|
|
811
|
+
"@mui/icons-material": { singleton: true } as any,
|
|
812
|
+
"@emotion/react": { singleton: true } as any,
|
|
813
|
+
"@emotion/styled": { singleton: true } as any
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
`;
|
|
818
|
+
}
|
|
819
|
+
function getSharedBadgeTsx() {
|
|
820
|
+
return `export default function SharedBadge() {
|
|
821
|
+
return <div className="section-card"><strong>SharedLib:</strong> SharedBadge loaded from sharedlib remote.</div>;
|
|
822
|
+
}
|
|
823
|
+
`;
|
|
824
|
+
}
|
|
825
|
+
function getMainFederationTypeDeclTs(children, sharedName) {
|
|
826
|
+
const lines = children.map(
|
|
827
|
+
(child) => `declare module ${JSON.stringify(`${child}/App`)} {
|
|
828
|
+
const RemoteApp: React.ComponentType;
|
|
829
|
+
export default RemoteApp;
|
|
830
|
+
}`
|
|
831
|
+
).join("\n\n");
|
|
832
|
+
return `/// <reference types="react" />
|
|
833
|
+
${lines}
|
|
834
|
+
declare module ${JSON.stringify(`${sharedName}/AppConfigProvider`)} {
|
|
835
|
+
const AppConfigProvider: React.ComponentType<{ children?: React.ReactNode }>;
|
|
836
|
+
export default AppConfigProvider;
|
|
837
|
+
}
|
|
838
|
+
declare module ${JSON.stringify(`${sharedName}/SettingsPage`)} {
|
|
839
|
+
const SettingsPage: React.ComponentType<{ section?: "theme" | "layout" | "branding" }>;
|
|
840
|
+
export default SettingsPage;
|
|
841
|
+
}
|
|
842
|
+
declare module ${JSON.stringify(`${sharedName}/AppNavbar`)} {
|
|
843
|
+
const AppNavbar: React.ComponentType<{ title?: string; children?: React.ReactNode }>;
|
|
844
|
+
export default AppNavbar;
|
|
845
|
+
}
|
|
846
|
+
`;
|
|
847
|
+
}
|
|
848
|
+
function getChildFederationTypeDeclTs(sharedName) {
|
|
849
|
+
return `/// <reference types="react" />
|
|
850
|
+
declare module ${JSON.stringify(`${sharedName}/SharedBadge`)} {
|
|
851
|
+
const SharedBadge: React.ComponentType;
|
|
852
|
+
export default SharedBadge;
|
|
853
|
+
}
|
|
854
|
+
`;
|
|
855
|
+
}
|
|
856
|
+
function getMainNavbarAppJsx(children, sharedName) {
|
|
857
|
+
const modules = JSON.stringify(children);
|
|
858
|
+
return `import { Link } from "react-router-dom";
|
|
859
|
+
import { AppRouter } from "./routes/AppRouter";
|
|
860
|
+
import "./styles/global.css";
|
|
861
|
+
|
|
862
|
+
export default function App() {
|
|
863
|
+
const modules = ${modules};
|
|
864
|
+
return (
|
|
865
|
+
<div className="app-shell">
|
|
866
|
+
<header className="app-header">
|
|
867
|
+
<h1>Main Microfrontend</h1>
|
|
868
|
+
<p>Module Federation Working</p>
|
|
869
|
+
<nav>
|
|
870
|
+
<Link to="/">home</Link>
|
|
871
|
+
{" | "}
|
|
872
|
+
{modules.map((name) => (
|
|
873
|
+
<span key={name}>
|
|
874
|
+
<Link to={\`/\${name}\`}>{name}</Link>{" | "}
|
|
875
|
+
</span>
|
|
876
|
+
))}
|
|
877
|
+
</nav>
|
|
878
|
+
</header>
|
|
879
|
+
<main className="app-content"><AppRouter /></main>
|
|
880
|
+
<footer className="app-footer"><span>Shared: ${sharedName}</span><span>Port: {import.meta.env.VITE_PORT}</span></footer>
|
|
881
|
+
</div>
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
`;
|
|
885
|
+
}
|
|
886
|
+
function getMainRouterJsx(children) {
|
|
887
|
+
const lazyImports = children.map((child) => `const ${toPascal(child)}Remote = React.lazy(() => import(${JSON.stringify(`${child}/App`)}));`).join("\n");
|
|
888
|
+
const routes = children.map((child) => `<Route path="/${child}/*" element={<React.Suspense fallback={<div>Loading ${child}...</div>}><${toPascal(child)}Remote /></React.Suspense>} />`).join("\n ");
|
|
889
|
+
return `import React from "react";
|
|
890
|
+
import { Navigate, Route, Routes } from "react-router-dom";
|
|
891
|
+
|
|
892
|
+
${lazyImports}
|
|
893
|
+
|
|
894
|
+
export function AppRouter() {
|
|
895
|
+
return (
|
|
896
|
+
<Routes>
|
|
897
|
+
<Route path="/" element={<div>Select a module from navbar.</div>} />
|
|
898
|
+
${routes}
|
|
899
|
+
<Route path="*" element={<Navigate to="/" replace />} />
|
|
900
|
+
</Routes>
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
function getChildAppJsx(appName, port, sharedName) {
|
|
906
|
+
return `import React from "react";
|
|
907
|
+
import { AppRouter } from "./routes/AppRouter";
|
|
908
|
+
import "./styles/global.css";
|
|
909
|
+
const SharedBadge = React.lazy(() => import(${JSON.stringify(`${sharedName}/SharedBadge`)}));
|
|
910
|
+
|
|
911
|
+
export default function App() {
|
|
912
|
+
return (
|
|
913
|
+
<div className="app-shell">
|
|
914
|
+
<header className="app-header">
|
|
915
|
+
<h1>${toPascal(appName)} Microfrontend</h1>
|
|
916
|
+
<p>Module Federation Working</p>
|
|
917
|
+
</header>
|
|
918
|
+
<main className="app-content">
|
|
919
|
+
<React.Suspense fallback={<div>Loading shared library...</div>}><SharedBadge /></React.Suspense>
|
|
920
|
+
<AppRouter />
|
|
921
|
+
</main>
|
|
922
|
+
<footer className="app-footer"><span>Port: ${port}</span></footer>
|
|
923
|
+
</div>
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
`;
|
|
927
|
+
}
|
|
928
|
+
function getChildFederationJs(appName, sharedName, sharedPort) {
|
|
929
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
930
|
+
|
|
931
|
+
export function buildFederation() {
|
|
932
|
+
return federation({
|
|
933
|
+
name: ${JSON.stringify(appName)},
|
|
934
|
+
filename: "remoteEntry.js",
|
|
935
|
+
exposes: { "./App": "./src/App.jsx" },
|
|
936
|
+
remotes: { ${JSON.stringify(sharedName)}: "http://localhost:${sharedPort}/assets/remoteEntry.js" },
|
|
937
|
+
shared: {
|
|
938
|
+
react: { singleton: true },
|
|
939
|
+
"react-dom": { singleton: true },
|
|
940
|
+
"react-router-dom": { singleton: true },
|
|
941
|
+
"@mui/material": { singleton: true },
|
|
942
|
+
"@mui/icons-material": { singleton: true },
|
|
943
|
+
"@emotion/react": { singleton: true },
|
|
944
|
+
"@emotion/styled": { singleton: true }
|
|
945
|
+
},
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
`;
|
|
949
|
+
}
|
|
950
|
+
function getSharedlibFederationJs(sharedName) {
|
|
951
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
952
|
+
|
|
953
|
+
export function buildFederation() {
|
|
954
|
+
return federation({
|
|
955
|
+
name: ${JSON.stringify(sharedName)},
|
|
956
|
+
filename: "remoteEntry.js",
|
|
957
|
+
exposes: {
|
|
958
|
+
"./App": "./src/App.jsx",
|
|
959
|
+
"./SharedBadge": "./src/components/layout/SharedBadge.jsx"
|
|
960
|
+
},
|
|
961
|
+
shared: {
|
|
962
|
+
react: { singleton: true },
|
|
963
|
+
"react-dom": { singleton: true },
|
|
964
|
+
"react-router-dom": { singleton: true },
|
|
965
|
+
"@mui/material": { singleton: true },
|
|
966
|
+
"@mui/icons-material": { singleton: true },
|
|
967
|
+
"@emotion/react": { singleton: true },
|
|
968
|
+
"@emotion/styled": { singleton: true }
|
|
969
|
+
},
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
`;
|
|
973
|
+
}
|
|
974
|
+
function getSharedBadgeJsx() {
|
|
975
|
+
return `export default function SharedBadge() {
|
|
976
|
+
return <div className="section-card"><strong>SharedLib:</strong> SharedBadge loaded from sharedlib remote.</div>;
|
|
977
|
+
}
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
980
|
+
function toPascal(value) {
|
|
981
|
+
return value.split(/[^a-zA-Z0-9]/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
982
|
+
}
|
|
983
|
+
function getSharedlibFoundationFilesTs() {
|
|
984
|
+
return {
|
|
985
|
+
"src/config/types.ts": `export type ColorMode = "light" | "dark" | "auto";
|
|
986
|
+
|
|
987
|
+
export interface AppConfig {
|
|
988
|
+
appName: string;
|
|
989
|
+
appTitle: string;
|
|
990
|
+
companyName: string;
|
|
991
|
+
navbarTitle: string;
|
|
992
|
+
logoUrl: string;
|
|
993
|
+
sidebarLogoUrl: string;
|
|
994
|
+
faviconUrl: string;
|
|
995
|
+
primaryColor: string;
|
|
996
|
+
secondaryColor: string;
|
|
997
|
+
backgroundColor: string;
|
|
998
|
+
sidebarBackground: string;
|
|
999
|
+
navbarBackground: string;
|
|
1000
|
+
cardBackground: string;
|
|
1001
|
+
textColor: string;
|
|
1002
|
+
hoverColor: string;
|
|
1003
|
+
borderColor: string;
|
|
1004
|
+
fontFamily: string;
|
|
1005
|
+
fontSize: number;
|
|
1006
|
+
headingWeight: number;
|
|
1007
|
+
bodyWeight: number;
|
|
1008
|
+
sidebarCollapsed: boolean;
|
|
1009
|
+
sidebarWidth: number;
|
|
1010
|
+
sidebarIconSize: number;
|
|
1011
|
+
sidebarTheme: "solid" | "glass" | "floating";
|
|
1012
|
+
compactMode: boolean;
|
|
1013
|
+
floatingSidebar: boolean;
|
|
1014
|
+
glassSidebar: boolean;
|
|
1015
|
+
navbarHeight: number;
|
|
1016
|
+
navbarBlur: boolean;
|
|
1017
|
+
navbarSticky: boolean;
|
|
1018
|
+
navbarTransparent: boolean;
|
|
1019
|
+
showSearch: boolean;
|
|
1020
|
+
showNotifications: boolean;
|
|
1021
|
+
showProfile: boolean;
|
|
1022
|
+
dashboardBackground: string;
|
|
1023
|
+
cardRadius: number;
|
|
1024
|
+
cardShadow: string;
|
|
1025
|
+
widgetSpacing: number;
|
|
1026
|
+
layoutDensity: "comfortable" | "compact";
|
|
1027
|
+
colorMode: ColorMode;
|
|
1028
|
+
roleAccess: Record<string, string[]>;
|
|
1029
|
+
featureFlags: Record<string, boolean>;
|
|
1030
|
+
tenantId: string;
|
|
1031
|
+
locale: string;
|
|
1032
|
+
rtl: boolean;
|
|
1033
|
+
}
|
|
1034
|
+
`,
|
|
1035
|
+
"src/config/defaultConfig.ts": `import type { AppConfig } from "./types";
|
|
1036
|
+
|
|
1037
|
+
export const themePresets: Record<string, Partial<AppConfig>> = {
|
|
1038
|
+
"Corporate Blue": { primaryColor: "#1976d2", secondaryColor: "#1565c0", navbarBackground: "#0f172a" },
|
|
1039
|
+
"Dark Enterprise": { colorMode: "dark", backgroundColor: "#0b1220", textColor: "#f1f5f9" },
|
|
1040
|
+
"Material Purple": { primaryColor: "#7e57c2", secondaryColor: "#9c27b0" },
|
|
1041
|
+
"Minimal Gray": { primaryColor: "#4b5563", secondaryColor: "#6b7280", cardShadow: "none" },
|
|
1042
|
+
"Modern Green": { primaryColor: "#10b981", secondaryColor: "#059669" },
|
|
1043
|
+
};
|
|
1044
|
+
|
|
1045
|
+
export const defaultConfig: AppConfig = {
|
|
1046
|
+
appName: "DWAT",
|
|
1047
|
+
appTitle: "Enterprise Microfrontend Platform",
|
|
1048
|
+
companyName: "Acme Corp",
|
|
1049
|
+
navbarTitle: "Control Center",
|
|
1050
|
+
logoUrl: "",
|
|
1051
|
+
sidebarLogoUrl: "",
|
|
1052
|
+
faviconUrl: "",
|
|
1053
|
+
primaryColor: "#1976d2",
|
|
1054
|
+
secondaryColor: "#9c27b0",
|
|
1055
|
+
backgroundColor: "#f5f5f5",
|
|
1056
|
+
sidebarBackground: "#111827",
|
|
1057
|
+
navbarBackground: "#0f172a",
|
|
1058
|
+
cardBackground: "#ffffff",
|
|
1059
|
+
textColor: "#111827",
|
|
1060
|
+
hoverColor: "#1d4ed8",
|
|
1061
|
+
borderColor: "#d1d5db",
|
|
1062
|
+
fontFamily: "Inter, system-ui, sans-serif",
|
|
1063
|
+
fontSize: 14,
|
|
1064
|
+
headingWeight: 700,
|
|
1065
|
+
bodyWeight: 400,
|
|
1066
|
+
sidebarCollapsed: false,
|
|
1067
|
+
sidebarWidth: 260,
|
|
1068
|
+
sidebarIconSize: 20,
|
|
1069
|
+
sidebarTheme: "solid",
|
|
1070
|
+
compactMode: false,
|
|
1071
|
+
floatingSidebar: false,
|
|
1072
|
+
glassSidebar: false,
|
|
1073
|
+
navbarHeight: 64,
|
|
1074
|
+
navbarBlur: true,
|
|
1075
|
+
navbarSticky: true,
|
|
1076
|
+
navbarTransparent: false,
|
|
1077
|
+
showSearch: true,
|
|
1078
|
+
showNotifications: true,
|
|
1079
|
+
showProfile: true,
|
|
1080
|
+
dashboardBackground: "#f5f5f5",
|
|
1081
|
+
cardRadius: 12,
|
|
1082
|
+
cardShadow: "0 8px 24px rgba(15,23,42,0.12)",
|
|
1083
|
+
widgetSpacing: 16,
|
|
1084
|
+
layoutDensity: "comfortable",
|
|
1085
|
+
colorMode: "light",
|
|
1086
|
+
roleAccess: { admin: ["theme:write", "branding:write"], viewer: ["theme:read"] },
|
|
1087
|
+
featureFlags: { commandPalette: true, analyticsWidget: true, pluginRegistry: true },
|
|
1088
|
+
tenantId: "default",
|
|
1089
|
+
locale: "en",
|
|
1090
|
+
rtl: false,
|
|
1091
|
+
};
|
|
1092
|
+
`,
|
|
1093
|
+
"src/config/configStore.ts": `import { defaultConfig, themePresets } from "./defaultConfig";
|
|
1094
|
+
import type { AppConfig } from "./types";
|
|
1095
|
+
|
|
1096
|
+
const STORAGE_KEY = "mf-enterprise-config";
|
|
1097
|
+
|
|
1098
|
+
export function loadConfig(): AppConfig {
|
|
1099
|
+
try {
|
|
1100
|
+
const raw = localStorage.getItem(STORAGE_KEY);
|
|
1101
|
+
if (!raw) return defaultConfig;
|
|
1102
|
+
return { ...defaultConfig, ...(JSON.parse(raw) as Partial<AppConfig>) };
|
|
1103
|
+
} catch {
|
|
1104
|
+
return defaultConfig;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
export function saveConfig(config: AppConfig): void {
|
|
1109
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
export function resetConfig(): AppConfig {
|
|
1113
|
+
saveConfig(defaultConfig);
|
|
1114
|
+
return defaultConfig;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export function applyPreset(config: AppConfig, presetName: string): AppConfig {
|
|
1118
|
+
const patch = themePresets[presetName] ?? {};
|
|
1119
|
+
const next = { ...config, ...patch };
|
|
1120
|
+
saveConfig(next);
|
|
1121
|
+
return next;
|
|
1122
|
+
}
|
|
1123
|
+
`,
|
|
1124
|
+
"src/config/themeGenerator.ts": `import { createTheme } from "@mui/material/styles";
|
|
1125
|
+
import type { AppConfig } from "./types";
|
|
1126
|
+
|
|
1127
|
+
export function buildMuiTheme(config: AppConfig) {
|
|
1128
|
+
return createTheme({
|
|
1129
|
+
direction: config.rtl ? "rtl" : "ltr",
|
|
1130
|
+
palette: {
|
|
1131
|
+
mode: config.colorMode === "dark" ? "dark" : "light",
|
|
1132
|
+
primary: { main: config.primaryColor },
|
|
1133
|
+
secondary: { main: config.secondaryColor },
|
|
1134
|
+
background: { default: config.backgroundColor, paper: config.cardBackground },
|
|
1135
|
+
text: { primary: config.textColor },
|
|
1136
|
+
},
|
|
1137
|
+
typography: {
|
|
1138
|
+
fontFamily: config.fontFamily,
|
|
1139
|
+
fontSize: config.fontSize,
|
|
1140
|
+
h1: { fontWeight: config.headingWeight },
|
|
1141
|
+
body1: { fontWeight: config.bodyWeight },
|
|
1142
|
+
},
|
|
1143
|
+
shape: { borderRadius: config.cardRadius },
|
|
1144
|
+
});
|
|
1145
|
+
}
|
|
1146
|
+
`,
|
|
1147
|
+
"src/config/AppConfigProvider.tsx": `import React, { createContext, useMemo, useState } from "react";
|
|
1148
|
+
import { CssBaseline, ThemeProvider } from "@mui/material";
|
|
1149
|
+
import { applyPreset, loadConfig, resetConfig, saveConfig } from "./configStore";
|
|
1150
|
+
import { buildMuiTheme } from "./themeGenerator";
|
|
1151
|
+
import type { AppConfig } from "./types";
|
|
1152
|
+
|
|
1153
|
+
type AppConfigContextValue = {
|
|
1154
|
+
config: AppConfig;
|
|
1155
|
+
updateConfig: (patch: Partial<AppConfig>) => void;
|
|
1156
|
+
setPreset: (name: string) => void;
|
|
1157
|
+
reset: () => void;
|
|
1158
|
+
exportJson: () => string;
|
|
1159
|
+
importJson: (payload: string) => void;
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
export const AppConfigContext = createContext<AppConfigContextValue | null>(null);
|
|
1163
|
+
|
|
1164
|
+
export default function AppConfigProvider({ children }: { children?: React.ReactNode }) {
|
|
1165
|
+
const [config, setConfig] = useState<AppConfig>(() => loadConfig());
|
|
1166
|
+
const theme = useMemo(() => buildMuiTheme(config), [config]);
|
|
1167
|
+
|
|
1168
|
+
const value = useMemo<AppConfigContextValue>(
|
|
1169
|
+
() => ({
|
|
1170
|
+
config,
|
|
1171
|
+
updateConfig: (patch) =>
|
|
1172
|
+
setConfig((prev) => {
|
|
1173
|
+
const next = { ...prev, ...patch };
|
|
1174
|
+
saveConfig(next);
|
|
1175
|
+
return next;
|
|
1176
|
+
}),
|
|
1177
|
+
setPreset: (name) =>
|
|
1178
|
+
setConfig((prev) => {
|
|
1179
|
+
const next = applyPreset(prev, name);
|
|
1180
|
+
return next;
|
|
1181
|
+
}),
|
|
1182
|
+
reset: () => setConfig(resetConfig()),
|
|
1183
|
+
exportJson: () => JSON.stringify(config, null, 2),
|
|
1184
|
+
importJson: (payload) =>
|
|
1185
|
+
setConfig((prev) => {
|
|
1186
|
+
const parsed = JSON.parse(payload) as Partial<AppConfig>;
|
|
1187
|
+
const next = { ...prev, ...parsed };
|
|
1188
|
+
saveConfig(next);
|
|
1189
|
+
return next;
|
|
1190
|
+
}),
|
|
1191
|
+
}),
|
|
1192
|
+
[config],
|
|
1193
|
+
);
|
|
1194
|
+
|
|
1195
|
+
return (
|
|
1196
|
+
<AppConfigContext.Provider value={value}>
|
|
1197
|
+
<ThemeProvider theme={theme}>
|
|
1198
|
+
<CssBaseline />
|
|
1199
|
+
{children}
|
|
1200
|
+
</ThemeProvider>
|
|
1201
|
+
</AppConfigContext.Provider>
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
`,
|
|
1205
|
+
"src/config/useAppConfig.ts": `import { useContext } from "react";
|
|
1206
|
+
import { AppConfigContext } from "./AppConfigProvider";
|
|
1207
|
+
|
|
1208
|
+
export function useAppConfig() {
|
|
1209
|
+
const ctx = useContext(AppConfigContext);
|
|
1210
|
+
if (!ctx) throw new Error("useAppConfig must be used within AppConfigProvider");
|
|
1211
|
+
return ctx;
|
|
1212
|
+
}
|
|
1213
|
+
`,
|
|
1214
|
+
"src/pages/Settings/SettingsPage.tsx": `import React from "react";
|
|
1215
|
+
import {
|
|
1216
|
+
Accordion,
|
|
1217
|
+
AccordionDetails,
|
|
1218
|
+
AccordionSummary,
|
|
1219
|
+
Box,
|
|
1220
|
+
Button,
|
|
1221
|
+
Card,
|
|
1222
|
+
CardContent,
|
|
1223
|
+
Grid,
|
|
1224
|
+
MenuItem,
|
|
1225
|
+
Stack,
|
|
1226
|
+
Tab,
|
|
1227
|
+
Tabs,
|
|
1228
|
+
TextField,
|
|
1229
|
+
Typography,
|
|
1230
|
+
} from "@mui/material";
|
|
1231
|
+
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
1232
|
+
import { themePresets } from "../../config/defaultConfig";
|
|
1233
|
+
import { useAppConfig } from "../../config/useAppConfig";
|
|
1234
|
+
|
|
1235
|
+
type Section = "theme" | "layout" | "branding";
|
|
1236
|
+
|
|
1237
|
+
export default function SettingsPage({ section = "theme" }: { section?: Section }) {
|
|
1238
|
+
const { config, updateConfig, setPreset, reset, exportJson, importJson } = useAppConfig();
|
|
1239
|
+
const [tab, setTab] = React.useState<Section>(section);
|
|
1240
|
+
const [importPayload, setImportPayload] = React.useState("");
|
|
1241
|
+
|
|
1242
|
+
return (
|
|
1243
|
+
<Card>
|
|
1244
|
+
<CardContent>
|
|
1245
|
+
<Typography variant="h5" gutterBottom>Enterprise Settings</Typography>
|
|
1246
|
+
<Tabs value={tab} onChange={(_, v) => setTab(v)}>
|
|
1247
|
+
<Tab value="theme" label="Theme" />
|
|
1248
|
+
<Tab value="layout" label="Layout" />
|
|
1249
|
+
<Tab value="branding" label="Branding" />
|
|
1250
|
+
</Tabs>
|
|
1251
|
+
|
|
1252
|
+
{tab === "branding" && (
|
|
1253
|
+
<Grid container spacing={2} sx={{ mt: 1 }}>
|
|
1254
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1255
|
+
<TextField fullWidth label="Application Title" value={config.appTitle} onChange={(e) => updateConfig({ appTitle: e.target.value })} />
|
|
1256
|
+
</Grid>
|
|
1257
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1258
|
+
<TextField fullWidth label="Company Name" value={config.companyName} onChange={(e) => updateConfig({ companyName: e.target.value })} />
|
|
1259
|
+
</Grid>
|
|
1260
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1261
|
+
<TextField fullWidth label="Logo URL" value={config.logoUrl} onChange={(e) => updateConfig({ logoUrl: e.target.value })} />
|
|
1262
|
+
</Grid>
|
|
1263
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1264
|
+
<TextField fullWidth label="Favicon URL" value={config.faviconUrl} onChange={(e) => updateConfig({ faviconUrl: e.target.value })} />
|
|
1265
|
+
</Grid>
|
|
1266
|
+
</Grid>
|
|
1267
|
+
)}
|
|
1268
|
+
|
|
1269
|
+
{tab === "theme" && (
|
|
1270
|
+
<Stack spacing={2} sx={{ mt: 2 }}>
|
|
1271
|
+
<Accordion defaultExpanded>
|
|
1272
|
+
<AccordionSummary expandIcon={<ExpandMoreIcon />}><Typography>Color System</Typography></AccordionSummary>
|
|
1273
|
+
<AccordionDetails>
|
|
1274
|
+
<Grid container spacing={2}>
|
|
1275
|
+
<Grid size={{ xs: 12, md: 4 }}><TextField fullWidth type="color" label="Primary" value={config.primaryColor} onChange={(e) => updateConfig({ primaryColor: e.target.value })} /></Grid>
|
|
1276
|
+
<Grid size={{ xs: 12, md: 4 }}><TextField fullWidth type="color" label="Secondary" value={config.secondaryColor} onChange={(e) => updateConfig({ secondaryColor: e.target.value })} /></Grid>
|
|
1277
|
+
<Grid size={{ xs: 12, md: 4 }}><TextField fullWidth type="color" label="Background" value={config.backgroundColor} onChange={(e) => updateConfig({ backgroundColor: e.target.value })} /></Grid>
|
|
1278
|
+
</Grid>
|
|
1279
|
+
</AccordionDetails>
|
|
1280
|
+
</Accordion>
|
|
1281
|
+
|
|
1282
|
+
<TextField select label="Preset Theme" value="" onChange={(e) => setPreset(e.target.value)}>
|
|
1283
|
+
{Object.keys(themePresets).map((preset) => (
|
|
1284
|
+
<MenuItem key={preset} value={preset}>{preset}</MenuItem>
|
|
1285
|
+
))}
|
|
1286
|
+
</TextField>
|
|
1287
|
+
</Stack>
|
|
1288
|
+
)}
|
|
1289
|
+
|
|
1290
|
+
{tab === "layout" && (
|
|
1291
|
+
<Grid container spacing={2} sx={{ mt: 1 }}>
|
|
1292
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1293
|
+
<TextField fullWidth type="number" label="Sidebar Width" value={config.sidebarWidth} onChange={(e) => updateConfig({ sidebarWidth: Number(e.target.value) })} />
|
|
1294
|
+
</Grid>
|
|
1295
|
+
<Grid size={{ xs: 12, md: 6 }}>
|
|
1296
|
+
<TextField fullWidth type="number" label="Navbar Height" value={config.navbarHeight} onChange={(e) => updateConfig({ navbarHeight: Number(e.target.value) })} />
|
|
1297
|
+
</Grid>
|
|
1298
|
+
</Grid>
|
|
1299
|
+
)}
|
|
1300
|
+
|
|
1301
|
+
<Box sx={{ mt: 3 }}>
|
|
1302
|
+
<Typography variant="subtitle2">Import / Export Config</Typography>
|
|
1303
|
+
<TextField multiline minRows={5} fullWidth value={importPayload} onChange={(e) => setImportPayload(e.target.value)} />
|
|
1304
|
+
<Stack direction="row" spacing={1} sx={{ mt: 1 }}>
|
|
1305
|
+
<Button variant="outlined" onClick={() => setImportPayload(exportJson())}>Export JSON</Button>
|
|
1306
|
+
<Button variant="outlined" onClick={() => importJson(importPayload)}>Import JSON</Button>
|
|
1307
|
+
<Button variant="contained" color="secondary" onClick={reset}>Reset to Default</Button>
|
|
1308
|
+
</Stack>
|
|
1309
|
+
</Box>
|
|
1310
|
+
</CardContent>
|
|
1311
|
+
</Card>
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
`,
|
|
1315
|
+
"src/shared-ui/AppNavbar/AppNavbar.tsx": `import { AppBar, Box, Toolbar, Typography } from "@mui/material";
|
|
1316
|
+
import { useAppConfig } from "../../config/useAppConfig";
|
|
1317
|
+
|
|
1318
|
+
export default function AppNavbar({ title, children }: { title?: string; children?: React.ReactNode }) {
|
|
1319
|
+
const { config } = useAppConfig();
|
|
1320
|
+
return (
|
|
1321
|
+
<AppBar position={config.navbarSticky ? "sticky" : "relative"} sx={{ bgcolor: config.navbarBackground, height: config.navbarHeight }}>
|
|
1322
|
+
<Toolbar sx={{ minHeight: config.navbarHeight }}>
|
|
1323
|
+
<Typography variant="h6" sx={{ mr: 2 }}>{title ?? config.navbarTitle}</Typography>
|
|
1324
|
+
<Box sx={{ display: "flex", gap: 1 }}>{children}</Box>
|
|
1325
|
+
</Toolbar>
|
|
1326
|
+
</AppBar>
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
`,
|
|
1330
|
+
"src/shared-ui/AppSidebar/AppSidebar.tsx": `import { Drawer } from "@mui/material";
|
|
1331
|
+
import { useAppConfig } from "../../config/useAppConfig";
|
|
1332
|
+
|
|
1333
|
+
export default function AppSidebar({ children }: { children?: React.ReactNode }) {
|
|
1334
|
+
const { config } = useAppConfig();
|
|
1335
|
+
return (
|
|
1336
|
+
<Drawer variant="permanent" sx={{ "& .MuiDrawer-paper": { width: config.sidebarWidth, bgcolor: config.sidebarBackground } }}>
|
|
1337
|
+
{children}
|
|
1338
|
+
</Drawer>
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
`,
|
|
1342
|
+
"src/shared-ui/ThemeProvider/ThemeProvider.tsx": `export { default } from "../../config/AppConfigProvider";
|
|
1343
|
+
`,
|
|
1344
|
+
"src/shared-ui/AppButton/AppButton.tsx": `export { Button as default } from "@mui/material";
|
|
1345
|
+
`,
|
|
1346
|
+
"src/shared-ui/AppCard/AppCard.tsx": `export { Card as default } from "@mui/material";
|
|
1347
|
+
`,
|
|
1348
|
+
"src/shared-ui/AppDialog/AppDialog.tsx": `export { Dialog as default } from "@mui/material";
|
|
1349
|
+
`,
|
|
1350
|
+
"src/shared-ui/AppTable/AppTable.tsx": `export { Table as default } from "@mui/material";
|
|
1351
|
+
`,
|
|
1352
|
+
"src/shared-ui/AppInput/AppInput.tsx": `export { TextField as default } from "@mui/material";
|
|
1353
|
+
`,
|
|
1354
|
+
"src/config/menuConfig.ts": `export const menuConfig = [{ id: "dashboard", label: "Dashboard", path: "/" }, { id: "settings", label: "Settings", path: "/settings/theme" }];
|
|
1355
|
+
`,
|
|
1356
|
+
"src/config/permissions.ts": `export const permissions = { admin: ["settings:read", "settings:write"], viewer: ["settings:read"] };
|
|
1357
|
+
`,
|
|
1358
|
+
"src/config/featureFlags.ts": `export const featureFlags = { commandPalette: true, analytics: true, auditLogs: true };
|
|
1359
|
+
`,
|
|
1360
|
+
"src/config/environment.ts": `export const environment = { apiBaseUrl: import.meta.env.VITE_API_URL || "http://localhost:8080/api" };
|
|
1361
|
+
`,
|
|
1362
|
+
"src/config/pluginRegistry.ts": `export const pluginRegistry = [{ name: "audit-log", enabled: true }, { name: "analytics-widget", enabled: true }];
|
|
1363
|
+
`,
|
|
1364
|
+
"src/components/Feedback/GlobalSnackbar.tsx": `import { Alert, Snackbar } from "@mui/material";
|
|
1365
|
+
import React from "react";
|
|
1366
|
+
export default function GlobalSnackbar() {
|
|
1367
|
+
const [open, setOpen] = React.useState(false);
|
|
1368
|
+
return <Snackbar open={open} onClose={() => setOpen(false)}><Alert severity="success">Global toast placeholder</Alert></Snackbar>;
|
|
1369
|
+
}
|
|
1370
|
+
`,
|
|
1371
|
+
"src/components/Guard/RouteGuard.tsx": `import { Navigate } from "react-router-dom";
|
|
1372
|
+
export default function RouteGuard({ allow = true, children }: { allow?: boolean; children: React.ReactNode }) {
|
|
1373
|
+
return allow ? <>{children}</> : <Navigate to="/" replace />;
|
|
1374
|
+
}
|
|
1375
|
+
`,
|
|
1376
|
+
"src/components/System/ErrorBoundary.tsx": `import React from "react";
|
|
1377
|
+
export default class ErrorBoundary extends React.Component<{ children: React.ReactNode }, { hasError: boolean }> {
|
|
1378
|
+
constructor(props: { children: React.ReactNode }) { super(props); this.state = { hasError: false }; }
|
|
1379
|
+
static getDerivedStateFromError() { return { hasError: true }; }
|
|
1380
|
+
render() { return this.state.hasError ? <div>Something went wrong.</div> : this.props.children; }
|
|
1381
|
+
}
|
|
1382
|
+
`,
|
|
1383
|
+
"src/components/System/LoadingScreen.tsx": `export default function LoadingScreen() { return <div style={{ padding: 24 }}>Loading enterprise platform...</div>; }
|
|
1384
|
+
`,
|
|
1385
|
+
"src/pages/CommandPalette/CommandPalette.tsx": `export default function CommandPalette() { return <div>Command palette placeholder</div>; }
|
|
1386
|
+
`,
|
|
1387
|
+
"src/pages/AuditLogs/AuditLogsPage.tsx": `export default function AuditLogsPage() { return <div>Audit logs UI placeholder</div>; }
|
|
1388
|
+
`,
|
|
1389
|
+
"src/pages/Analytics/AnalyticsWidgetPage.tsx": `export default function AnalyticsWidgetPage() { return <div>Analytics widget placeholder</div>; }
|
|
1390
|
+
`,
|
|
1391
|
+
"src/pages/Notifications/NotificationCenter.tsx": `export default function NotificationCenter() { return <div>Notification center placeholder</div>; }
|
|
1392
|
+
`,
|
|
1393
|
+
"src/pages/Profile/ProfileDropdown.tsx": `export default function ProfileDropdown() { return <div>Profile dropdown placeholder</div>; }
|
|
1394
|
+
`,
|
|
1395
|
+
"src/i18n/index.ts": `export const locales = ["en", "ar"]; export const t = (k: string) => k;
|
|
1396
|
+
`
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
function getHostFederationTs(remotes) {
|
|
1400
|
+
const remotesBlock = Object.entries(remotes).map(([name, port]) => ` ${JSON.stringify(name)}: "http://localhost:${port}/assets/remoteEntry.js",`).join("\n");
|
|
1401
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
1402
|
+
|
|
1403
|
+
export function buildFederation() {
|
|
1404
|
+
return federation({
|
|
1405
|
+
name: "main",
|
|
1406
|
+
filename: "remoteEntry.js",
|
|
1407
|
+
remotes: {
|
|
1408
|
+
${remotesBlock}
|
|
1409
|
+
},
|
|
1410
|
+
shared: {
|
|
1411
|
+
react: { singleton: true } as any,
|
|
1412
|
+
"react-dom": { singleton: true } as any,
|
|
1413
|
+
"react-router-dom": { singleton: true } as any,
|
|
1414
|
+
"@mui/material": { singleton: true } as any,
|
|
1415
|
+
"@mui/icons-material": { singleton: true } as any,
|
|
1416
|
+
"@emotion/react": { singleton: true } as any,
|
|
1417
|
+
"@emotion/styled": { singleton: true } as any
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
}
|
|
1421
|
+
`;
|
|
1422
|
+
}
|
|
1423
|
+
function getHostFederationJs(remotes) {
|
|
1424
|
+
const remotesBlock = Object.entries(remotes).map(([name, port]) => ` ${JSON.stringify(name)}: "http://localhost:${port}/assets/remoteEntry.js",`).join("\n");
|
|
1425
|
+
return `import federation from "@originjs/vite-plugin-federation";
|
|
1426
|
+
|
|
1427
|
+
export function buildFederation() {
|
|
1428
|
+
return federation({
|
|
1429
|
+
name: "main",
|
|
1430
|
+
filename: "remoteEntry.js",
|
|
1431
|
+
remotes: {
|
|
1432
|
+
${remotesBlock}
|
|
1433
|
+
},
|
|
1434
|
+
shared: {
|
|
1435
|
+
react: { singleton: true },
|
|
1436
|
+
"react-dom": { singleton: true },
|
|
1437
|
+
"react-router-dom": { singleton: true },
|
|
1438
|
+
"@mui/material": { singleton: true },
|
|
1439
|
+
"@mui/icons-material": { singleton: true },
|
|
1440
|
+
"@emotion/react": { singleton: true },
|
|
1441
|
+
"@emotion/styled": { singleton: true }
|
|
1442
|
+
},
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
`;
|
|
1446
|
+
}
|
|
1447
|
+
function getWorkspacePackageJson(projectName, allApps, remotes) {
|
|
1448
|
+
const envs = ["dev", "staging", "uat", "production"];
|
|
1449
|
+
const hostApp = allApps[0];
|
|
1450
|
+
const sharedApp = remotes.find((name) => name.includes("shared")) || remotes[0];
|
|
1451
|
+
const scripts = {
|
|
1452
|
+
local: "npm run dev:watch",
|
|
1453
|
+
dev: "node print-env.js && npm-run-all --parallel dev-run:*",
|
|
1454
|
+
"dev:watch": "npm-run-all build:local && npm-run-all --parallel watch preview live-reload",
|
|
1455
|
+
"dev:fast": "npm run build:dev && npm-run-all --parallel preview live-reload",
|
|
1456
|
+
"dev:safe": "npm run build:production && npm-run-all --parallel preview",
|
|
1457
|
+
"live-reload": "node live-reload.js",
|
|
1458
|
+
watch: `npm-run-all --parallel ${allApps.map((a) => `watch:${a}`).join(" ")}`,
|
|
1459
|
+
preview: `npm-run-all --parallel ${allApps.map((a) => `preview:${a}`).join(" ")}`,
|
|
1460
|
+
build: "npm run build:production",
|
|
1461
|
+
release: "node scripts/bump-version.js patch --commit --push && npm run build",
|
|
1462
|
+
"release:minor": "node scripts/bump-version.js minor --commit --push && npm run build",
|
|
1463
|
+
"release:major": "node scripts/bump-version.js major --commit --push && npm run build",
|
|
1464
|
+
"build:local": allApps.map((a) => `npm run build:local:${a}`).join(" && ")
|
|
1465
|
+
};
|
|
1466
|
+
scripts["build:localdev"] = allApps.map((a) => `npm run build:local:${a}`).join(" && ");
|
|
1467
|
+
allApps.forEach((app) => {
|
|
1468
|
+
scripts[`dev-run:${app}`] = `npm --workspace apps/${app} run dev`;
|
|
1469
|
+
scripts[`watch:${app}`] = `npm --workspace apps/${app} run watch`;
|
|
1470
|
+
scripts[`preview:${app}`] = `npm --workspace apps/${app} run preview`;
|
|
1471
|
+
});
|
|
1472
|
+
envs.forEach((env) => {
|
|
1473
|
+
const mode = env === "production" ? "prod" : env;
|
|
1474
|
+
scripts[`build:${env}`] = allApps.map((app) => `npm run build:${env}:${app}`).join(" && ");
|
|
1475
|
+
allApps.forEach((app) => {
|
|
1476
|
+
scripts[`build:${env}:${app}`] = `npm --workspace apps/${app} run build -- --mode ${mode}`;
|
|
1477
|
+
});
|
|
1478
|
+
});
|
|
1479
|
+
allApps.forEach((app) => {
|
|
1480
|
+
scripts[`build:local:${app}`] = `npm --workspace apps/${app} run build -- --mode localdev`;
|
|
1481
|
+
});
|
|
1482
|
+
const scenarioApps = remotes.filter((name) => name !== sharedApp);
|
|
1483
|
+
scenarioApps.forEach((targetApp) => {
|
|
1484
|
+
const buildList = Array.from(/* @__PURE__ */ new Set([sharedApp, ...remotes.filter((name) => name === targetApp), hostApp])).filter(Boolean);
|
|
1485
|
+
const watchList = buildList.map((app) => `watch:local:${app}`).join(" ");
|
|
1486
|
+
const previewList = buildList.map((app) => `preview:local:${app}`).join(" ");
|
|
1487
|
+
scripts[`${targetApp}`] = `npm-run-all build:local:${targetApp}-only && npm-run-all --parallel watch:local:${targetApp}-only ${previewList} live-reload`;
|
|
1488
|
+
scripts[`build:local:${targetApp}-only`] = buildList.map((app) => `npm run build:local:${app}`).join(" && ");
|
|
1489
|
+
scripts[`watch:local:${targetApp}-only`] = `npm-run-all --parallel ${watchList}`;
|
|
1490
|
+
});
|
|
1491
|
+
allApps.forEach((app) => {
|
|
1492
|
+
scripts[`watch:local:${app}`] = `npm --workspace apps/${app} run watch -- --mode localdev`;
|
|
1493
|
+
scripts[`preview:local:${app}`] = `npm --workspace apps/${app} run preview:local`;
|
|
1494
|
+
});
|
|
1495
|
+
const packageJson = {
|
|
1496
|
+
name: projectName,
|
|
1497
|
+
private: true,
|
|
1498
|
+
type: "module",
|
|
1499
|
+
workspaces: ["apps/*"],
|
|
1500
|
+
scripts,
|
|
1501
|
+
devDependencies: {
|
|
1502
|
+
"@originjs/vite-plugin-federation": "^1.4.1",
|
|
1503
|
+
"npm-run-all": "^4.1.5",
|
|
1504
|
+
"vite-plugin-live-reload": "^3.0.5",
|
|
1505
|
+
"vite-plugin-static-copy": "^3.2.0",
|
|
1506
|
+
ws: "^8.19.0"
|
|
1507
|
+
},
|
|
1508
|
+
mfruntime: {
|
|
1509
|
+
host: "main",
|
|
1510
|
+
remotes
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
return `${JSON.stringify(packageJson, null, 2)}
|
|
1514
|
+
`;
|
|
1515
|
+
}
|
|
1516
|
+
function getWorkspaceReadme(projectName, portMap) {
|
|
1517
|
+
const appsBlock = Object.entries(portMap).map(([name, port]) => `- ${name}: ${port}`).join("\n");
|
|
1518
|
+
return `# ${projectName}
|
|
1519
|
+
|
|
1520
|
+
Enterprise microfrontend monorepo generated by create-mf-app.
|
|
1521
|
+
|
|
1522
|
+
## Apps and ports
|
|
1523
|
+
${appsBlock}
|
|
1524
|
+
|
|
1525
|
+
## Quick start
|
|
1526
|
+
\`\`\`bash
|
|
1527
|
+
npm install
|
|
1528
|
+
npm run dev
|
|
1529
|
+
\`\`\`
|
|
1530
|
+
`;
|
|
1531
|
+
}
|
|
1532
|
+
function getRootEnvFile(projectName, portMap) {
|
|
1533
|
+
const appVars = Object.entries(portMap).map(([app, port]) => {
|
|
1534
|
+
const key = app.toUpperCase().replace(/[^A-Z0-9]/g, "_");
|
|
1535
|
+
return `VITE_${key}_PORT=${port}
|
|
1536
|
+
VITE_${key}_REMOTE_ENTRY=http://localhost:${port}/assets/remoteEntry.js`;
|
|
1537
|
+
}).join("\n");
|
|
1538
|
+
return `VITE_WORKSPACE_NAME=${projectName}
|
|
1539
|
+
VITE_DEFAULT_MODE=localdev
|
|
1540
|
+
${appVars}
|
|
1541
|
+
`;
|
|
1542
|
+
}
|
|
1543
|
+
function getDockerCompose(allApps, portMap, enabled) {
|
|
1544
|
+
if (!enabled) {
|
|
1545
|
+
return "services: {}\n";
|
|
1546
|
+
}
|
|
1547
|
+
const services = allApps.map(
|
|
1548
|
+
(app) => ` ${app}:
|
|
1549
|
+
build: ./apps/${app}
|
|
1550
|
+
ports:
|
|
1551
|
+
- "${portMap[app]}:${portMap[app]}"`
|
|
1552
|
+
).join("\n\n");
|
|
1553
|
+
return `services:
|
|
1554
|
+
${services}
|
|
1555
|
+
`;
|
|
1556
|
+
}
|
|
1557
|
+
function getPrintEnvScript() {
|
|
1558
|
+
return `console.log("Environment:", process.env.MODE || process.env.NODE_ENV || "localdev");
|
|
1559
|
+
`;
|
|
1560
|
+
}
|
|
1561
|
+
function getSmartBuildScript() {
|
|
1562
|
+
return `const mode = process.argv[2] || "dev";
|
|
1563
|
+
console.log("Smart build mode:", mode);
|
|
1564
|
+
`;
|
|
1565
|
+
}
|
|
1566
|
+
function getLiveReloadScript() {
|
|
1567
|
+
return `console.log("Live reload bridge started (placeholder).");
|
|
1568
|
+
setInterval(() => {}, 1 << 30);
|
|
1569
|
+
`;
|
|
1570
|
+
}
|
|
1571
|
+
function getBumpVersionScript() {
|
|
1572
|
+
return `#!/usr/bin/env node
|
|
1573
|
+
const fs = require("node:fs");
|
|
1574
|
+
const path = require("node:path");
|
|
1575
|
+
const type = process.argv[2] || "patch";
|
|
1576
|
+
const file = path.join(process.cwd(), "package.json");
|
|
1577
|
+
const pkg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1578
|
+
const [maj, min, pat] = pkg.version.split(".").map(Number);
|
|
1579
|
+
if (type === "major") pkg.version = \`\${maj + 1}.0.0\`;
|
|
1580
|
+
else if (type === "minor") pkg.version = \`\${maj}.\${min + 1}.0\`;
|
|
1581
|
+
else pkg.version = \`\${maj}.\${min}.\${pat + 1}\`;
|
|
1582
|
+
fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + "\\n");
|
|
1583
|
+
console.log("Version bumped to", pkg.version);
|
|
1584
|
+
`;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// src/utils/args.ts
|
|
1588
|
+
function parseArgv(argv) {
|
|
1589
|
+
const positionals = [];
|
|
1590
|
+
const options = {};
|
|
1591
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
1592
|
+
const arg = argv[i];
|
|
1593
|
+
if (!arg.startsWith("-")) {
|
|
1594
|
+
positionals.push(arg);
|
|
1595
|
+
continue;
|
|
1596
|
+
}
|
|
1597
|
+
if (arg === "-h" || arg === "--help") {
|
|
1598
|
+
options.help = true;
|
|
1599
|
+
continue;
|
|
1600
|
+
}
|
|
1601
|
+
if (arg === "-y" || arg === "--yes") {
|
|
1602
|
+
options.yes = true;
|
|
1603
|
+
continue;
|
|
1604
|
+
}
|
|
1605
|
+
const normalized = arg.replace(/^--/, "");
|
|
1606
|
+
const eqIndex = normalized.indexOf("=");
|
|
1607
|
+
if (eqIndex >= 0) {
|
|
1608
|
+
const key2 = normalized.slice(0, eqIndex);
|
|
1609
|
+
const value = normalized.slice(eqIndex + 1);
|
|
1610
|
+
options[key2] = value || true;
|
|
1611
|
+
continue;
|
|
1612
|
+
}
|
|
1613
|
+
const key = normalized;
|
|
1614
|
+
const next = argv[i + 1];
|
|
1615
|
+
if (next && !next.startsWith("-")) {
|
|
1616
|
+
const parts = [next];
|
|
1617
|
+
i += 1;
|
|
1618
|
+
while (argv[i + 1] && !argv[i + 1].startsWith("-")) {
|
|
1619
|
+
parts.push(argv[i + 1]);
|
|
1620
|
+
i += 1;
|
|
1621
|
+
}
|
|
1622
|
+
options[key] = parts.join(",");
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
options[key] = true;
|
|
1626
|
+
}
|
|
1627
|
+
return { positionals, options };
|
|
1628
|
+
}
|
|
1629
|
+
function toBoolean(value, defaultValue) {
|
|
1630
|
+
if (value === void 0) {
|
|
1631
|
+
return defaultValue;
|
|
1632
|
+
}
|
|
1633
|
+
if (value === true || value === "true" || value === "1") {
|
|
1634
|
+
return true;
|
|
1635
|
+
}
|
|
1636
|
+
if (value === "false" || value === "0") {
|
|
1637
|
+
return false;
|
|
1638
|
+
}
|
|
1639
|
+
return defaultValue;
|
|
1640
|
+
}
|
|
1641
|
+
function resolveTemplate(options) {
|
|
1642
|
+
const flagTs = options.typescript === true;
|
|
1643
|
+
const flagJs = options.javascript === true;
|
|
1644
|
+
const templateValue = options.template;
|
|
1645
|
+
if (flagTs && flagJs) {
|
|
1646
|
+
throw new Error("Use only one template flag: --typescript or --javascript");
|
|
1647
|
+
}
|
|
1648
|
+
if (typeof templateValue === "string") {
|
|
1649
|
+
if (templateValue === "tsx" || templateValue === "jsx") {
|
|
1650
|
+
return templateValue;
|
|
1651
|
+
}
|
|
1652
|
+
throw new Error("Invalid --template value. Use --template=tsx or --template=jsx");
|
|
1653
|
+
}
|
|
1654
|
+
if (flagTs) {
|
|
1655
|
+
return "tsx";
|
|
1656
|
+
}
|
|
1657
|
+
if (flagJs) {
|
|
1658
|
+
return "jsx";
|
|
1659
|
+
}
|
|
1660
|
+
return "tsx";
|
|
1661
|
+
}
|
|
1662
|
+
function resolveAppType(options) {
|
|
1663
|
+
return options.type === "parent" ? "parent" : "child";
|
|
1664
|
+
}
|
|
1665
|
+
function resolveChildren(options) {
|
|
1666
|
+
const value = options.children;
|
|
1667
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1668
|
+
return [];
|
|
1669
|
+
}
|
|
1670
|
+
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
1671
|
+
}
|
|
1672
|
+
function resolveShared(options) {
|
|
1673
|
+
const value = options.shared;
|
|
1674
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
1675
|
+
return [];
|
|
1676
|
+
}
|
|
1677
|
+
return value.split(/[,\s]+/).map((item) => item.trim()).filter(Boolean);
|
|
1678
|
+
}
|
|
1679
|
+
function buildConfigFromArgs(argv) {
|
|
1680
|
+
const { options, positionals } = parseArgv(argv);
|
|
1681
|
+
if (options.help) {
|
|
1682
|
+
return { help: true, interactive: false };
|
|
1683
|
+
}
|
|
1684
|
+
const projectName = positionals[0];
|
|
1685
|
+
const interactive = !projectName && options.yes !== true;
|
|
1686
|
+
if (interactive) {
|
|
1687
|
+
return { help: false, interactive: true };
|
|
1688
|
+
}
|
|
1689
|
+
const resolvedProjectName = projectName || "mf-app";
|
|
1690
|
+
const rawPort = String(options.port ?? "3001");
|
|
1691
|
+
const port = Number(rawPort);
|
|
1692
|
+
if (!Number.isFinite(port) || port <= 0) {
|
|
1693
|
+
throw new Error("Invalid port provided. Example: --port=3001");
|
|
1694
|
+
}
|
|
1695
|
+
return {
|
|
1696
|
+
help: false,
|
|
1697
|
+
interactive: false,
|
|
1698
|
+
config: {
|
|
1699
|
+
projectName: resolvedProjectName,
|
|
1700
|
+
port,
|
|
1701
|
+
docker: toBoolean(options.docker, true),
|
|
1702
|
+
federation: toBoolean(options.federation, true),
|
|
1703
|
+
template: resolveTemplate(options),
|
|
1704
|
+
appType: resolveAppType(options),
|
|
1705
|
+
children: resolveChildren(options),
|
|
1706
|
+
shared: resolveShared(options)
|
|
1707
|
+
}
|
|
1708
|
+
};
|
|
1709
|
+
}
|
|
1710
|
+
function isValidProjectName(name) {
|
|
1711
|
+
return /^[a-z0-9-]+$/i.test(name);
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
// src/index.ts
|
|
1715
|
+
function printHelp() {
|
|
1716
|
+
console.log("Usage:");
|
|
1717
|
+
console.log(" npx create-mf-app <project-name> [options]");
|
|
1718
|
+
console.log(" npx create-mf-app");
|
|
1719
|
+
console.log("");
|
|
1720
|
+
console.log("Options:");
|
|
1721
|
+
console.log(" --port=3001");
|
|
1722
|
+
console.log(" --template=tsx|jsx");
|
|
1723
|
+
console.log(" --typescript");
|
|
1724
|
+
console.log(" --javascript");
|
|
1725
|
+
console.log(" --type=parent|child");
|
|
1726
|
+
console.log(" --children=admin,qc,inventory");
|
|
1727
|
+
console.log(" --shared=sharedlib");
|
|
1728
|
+
console.log(" --docker=true|false");
|
|
1729
|
+
console.log(" --federation=true|false");
|
|
1730
|
+
console.log(" -y, --yes");
|
|
1731
|
+
console.log(" -h, --help");
|
|
1732
|
+
}
|
|
1733
|
+
function assertConfig(config) {
|
|
1734
|
+
if (!isValidProjectName(config.projectName)) {
|
|
1735
|
+
throw new Error("Invalid project name. Use letters, numbers, and dashes only.");
|
|
1736
|
+
}
|
|
1737
|
+
if (!Number.isFinite(config.port) || config.port <= 0) {
|
|
1738
|
+
throw new Error("Invalid port. Example: --port=3001");
|
|
1739
|
+
}
|
|
1740
|
+
if (config.appType === "parent" && config.children.length === 0) {
|
|
1741
|
+
throw new Error("Parent mode requires children. Example: --children=admin,qc,inventory");
|
|
1742
|
+
}
|
|
1743
|
+
if (config.appType === "parent" && config.shared.length === 0) {
|
|
1744
|
+
config.shared = ["sharedlib"];
|
|
1745
|
+
}
|
|
1746
|
+
if (config.appType === "parent" && config.children.includes("main")) {
|
|
1747
|
+
throw new Error('"main" is reserved as the mandatory host app. Do not include it in --children.');
|
|
1748
|
+
}
|
|
1749
|
+
if (config.appType === "parent" && config.shared.includes("main")) {
|
|
1750
|
+
throw new Error('"main" is reserved as the mandatory host app. Do not include it in --shared.');
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
async function run() {
|
|
1754
|
+
const parsed = buildConfigFromArgs(process.argv.slice(2));
|
|
1755
|
+
if (parsed.help) {
|
|
1756
|
+
printHelp();
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const config = parsed.interactive ? await promptCliConfig() : parsed.config;
|
|
1760
|
+
assertConfig(config);
|
|
1761
|
+
await assertTargetDirectoryDoesNotExist(config);
|
|
1762
|
+
await generateProject(config);
|
|
1763
|
+
console.log("\nProject created successfully.");
|
|
1764
|
+
console.log(`
|
|
1765
|
+
Next steps:
|
|
1766
|
+
cd ${config.projectName}
|
|
1767
|
+
npm install
|
|
1768
|
+
npm run dev`);
|
|
1769
|
+
}
|
|
1770
|
+
run().catch((error) => {
|
|
1771
|
+
console.error("\nFailed to create project.");
|
|
1772
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1773
|
+
process.exit(1);
|
|
1774
|
+
});
|