@fulgurjs/federation 0.4.2

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/dist/cli.js ADDED
@@ -0,0 +1,650 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/config.ts
13
+ async function loadFulgurConfig(configPath) {
14
+ const { pathToFileURL, fileURLToPath } = await import("node:url");
15
+ const { createRequire } = await import("node:module");
16
+ const fs2 = await import("node:fs");
17
+ const os = await import("node:os");
18
+ const path2 = await import("node:path");
19
+ const selfDir = path2.dirname(fileURLToPath(import.meta.url));
20
+ const selfConfigJs = path2.join(selfDir, "config.js");
21
+ const SELF_SPEC = fs2.existsSync(selfConfigJs) ? pathToFileURL(selfConfigJs).href : "@fulgur-federation-config-unavailable";
22
+ const rewriteSelfImports = (source) => source.replace(/(['"])@fulgur\/federation\/config\1/g, (_m, q) => `${q}${SELF_SPEC}${q}`);
23
+ const importConfigs = async (cacheKey) => {
24
+ return await import(`${cacheKey}?t=${Date.now()}`);
25
+ };
26
+ let mod;
27
+ if (configPath.endsWith(".json")) {
28
+ mod = { default: JSON.parse(fs2.readFileSync(configPath, "utf8")) };
29
+ } else {
30
+ const source = rewriteSelfImports(fs2.readFileSync(configPath, "utf8"));
31
+ const tmpBase = path2.join(os.tmpdir(), `fulgur-config-${Date.now()}`);
32
+ if (/\.(ts|mts|cts)$/.test(configPath)) {
33
+ try {
34
+ const tmpTs = `${tmpBase}.ts`;
35
+ fs2.writeFileSync(tmpTs, source);
36
+ mod = await importConfigs(pathToFileURL(tmpTs).href);
37
+ } catch (e) {
38
+ if (!/Unexpected|SyntaxError|ERR_UNSUPPORTED_NODE_MODULES|ERR_UNKNOWN_FILE_EXTENSION|ERR_IMPORT_ATTRIBUTE/.test(String(e)) && process.version.startsWith("v2")) {
39
+ throw e;
40
+ }
41
+ const requireFromConfig = createRequire(configPath);
42
+ let esbuild;
43
+ try {
44
+ esbuild = requireFromConfig("esbuild");
45
+ } catch {
46
+ const requireFromSelf = createRequire(import.meta.url);
47
+ esbuild = requireFromSelf("esbuild");
48
+ }
49
+ const out = esbuild.buildSync({ entryPoints: [configPath], bundle: false, write: false, format: "esm", target: "node18" });
50
+ const jsFile = `${tmpBase}.mjs`;
51
+ fs2.writeFileSync(jsFile, rewriteSelfImports(out.outputFiles[0].text));
52
+ mod = await importConfigs(pathToFileURL(jsFile).href);
53
+ }
54
+ } else {
55
+ const jsFile = `${tmpBase}.mjs`;
56
+ fs2.writeFileSync(jsFile, source);
57
+ mod = await importConfigs(pathToFileURL(jsFile).href);
58
+ }
59
+ }
60
+ const cfg = mod.default;
61
+ if (!cfg?.root) {
62
+ throw new Error(
63
+ `[fulgur:init] \u914D\u7F6E\u7F3A\u5C11 root\uFF08\u5DE5\u7A0B\u6839\u76EE\u5F55\u7EDD\u5BF9\u8DEF\u5F84\uFF09
64
+ \u6839\u56E0\uFF1ACLI \u9700\u8981\u77E5\u9053\u5728\u54EA\u4E2A\u5DE5\u7A0B\u4E0A\u505A\u96C6\u6210
65
+ \u4FEE\u6CD5\uFF1Afulgur.config.ts \u9876\u5C42\u8865 root: '/abs/path/to/repo'\uFF08\u8D77\u6B65\u6A21\u677F\uFF1Afulgur init\uFF09`
66
+ );
67
+ }
68
+ if (!Array.isArray(cfg.apps) || cfg.apps.length === 0) {
69
+ throw new Error("[fulgur:init] \u914D\u7F6E\u7F3A\u5C11 apps\uFF08\u81F3\u5C11\u4E00\u4E2A\u5BBF\u4E3B\u6216\u8FDC\u7A0B\u5E94\u7528\uFF09");
70
+ }
71
+ for (const app of cfg.apps) {
72
+ if (!app.host && !app.remote) {
73
+ throw new Error(
74
+ `[fulgur:init] \u5E94\u7528 "${app.path}" \u65E2\u65E0 host \u4E5F\u65E0 remote \u89D2\u8272
75
+ \u6839\u56E0\uFF1A\u5E94\u7528\u5FC5\u987B\u81F3\u5C11\u58F0\u660E\u4E00\u4E2A\u89D2\u8272
76
+ \u4FEE\u6CD5\uFF1A\u5BBF\u4E3B\u8865 host: { pages, remotePrefixes, remotes }\uFF0C\u8FDC\u7A0B\u8865 remote: { exposes }`
77
+ );
78
+ }
79
+ }
80
+ return cfg;
81
+ }
82
+ var init_config = __esm({
83
+ "src/config.ts"() {
84
+ "use strict";
85
+ }
86
+ });
87
+
88
+ // src/init.ts
89
+ var init_exports = {};
90
+ __export(init_exports, {
91
+ STARTER_CONFIG: () => STARTER_CONFIG,
92
+ inspectConfig: () => inspectConfig,
93
+ writeConfigTemplate: () => writeConfigTemplate
94
+ });
95
+ import fs from "node:fs";
96
+ import path from "node:path";
97
+ async function writeConfigTemplate(target, force = false) {
98
+ if (fs.existsSync(target) && !force) {
99
+ return "exists";
100
+ }
101
+ fs.mkdirSync(path.dirname(path.resolve(target)), { recursive: true });
102
+ fs.writeFileSync(path.resolve(target), STARTER_CONFIG);
103
+ return "written";
104
+ }
105
+ function remoteEntriesLiteral(remotes) {
106
+ return Object.entries(remotes).map(([k, v]) => ` '${k}': { dev: '${v.dev}', prod: '${v.prod}' },`).join("\n");
107
+ }
108
+ function exposesLiteral(exposes) {
109
+ return Object.entries(exposes).map(([k, v]) => ` '${k}': '${v}',`).join("\n");
110
+ }
111
+ function viteSnippetFor(app) {
112
+ const hostRemotes = app.host ? remoteEntriesLiteral(app.host.remotes) : "";
113
+ const remoteRemotes = app.remote?.remotes ? remoteEntriesLiteral(app.remote.remotes) : "";
114
+ const remotesBlock = hostRemotes || remoteRemotes ? ` remotes: {
115
+ ${hostRemotes || remoteRemotes}
116
+ },
117
+ ` : "";
118
+ const exposesBlock = app.remote?.exposes ? ` exposes: {
119
+ ${exposesLiteral(app.remote.exposes)}
120
+ },
121
+ ` : "";
122
+ const sharedBlock = app.shared ? ` shared: {
123
+ ${Object.entries(app.shared).map(([k, v]) => {
124
+ const opts = [v.singleton ? "singleton: true" : "", v.requiredVersion ? `requiredVersion: '${v.requiredVersion}'` : ""].filter(Boolean);
125
+ return ` '${k}': { ${opts.join(", ")} },`;
126
+ }).join("\n")}
127
+ },
128
+ ` : ` ${DEFAULT_SHARED_BLOCK},
129
+ `;
130
+ return `federation({
131
+ name: '${app.name}',
132
+ ${remotesBlock}${exposesBlock}${sharedBlock}})`;
133
+ }
134
+ function nginxSnippetFor(cfg) {
135
+ const listen = cfg.deploy?.listen ?? 8080;
136
+ const webRoot = cfg.deploy?.webRoot ?? "/var/www/your-site";
137
+ const hostApp = cfg.apps.find((a) => a.host) ?? cfg.apps[0];
138
+ const hostIndex = `/${hostApp.base.replace(/^\/|\/$/g, "")}/index.html`.replace(/\/{2,}/g, "/");
139
+ const blocks = cfg.apps.map(
140
+ (a) => {
141
+ const b = a.base === "/" || a.base === "" ? "/" : a.base.replace(/\/$/, "");
142
+ return ` location ${b} {
143
+ try_files $uri $uri/ ${hostIndex};
144
+
145
+ location = ${b}/fulgur-remoteEntry.js {
146
+ add_header Cache-Control "no-cache";
147
+ add_header Access-Control-Allow-Origin "*";
148
+ }
149
+ location = ${b}/fulgur-manifest.json {
150
+ add_header Cache-Control "no-cache";
151
+ add_header Access-Control-Allow-Origin "*";
152
+ }
153
+ location = ${b}/index.html {
154
+ add_header Cache-Control "no-cache";
155
+ }
156
+ }`;
157
+ }
158
+ ).join("\n\n");
159
+ return `# \u8054\u90A6\u7AD9\u70B9\u6A21\u677F\uFF08\u8981\u70B9\uFF1AremoteEntry/manifest/index.html \u5FC5\u987B no-cache\u2014\u2014
160
+ # \u6587\u4EF6\u540D\u56FA\u5B9A\u800C\u5185\u5BB9\u6BCF\u6B21\u6784\u5EFA\u53D8\u5316\uFF0Cimmutable \u957F\u7F13\u5B58\u4F1A\u8BA9\u6D4F\u89C8\u5668\u6301\u65E7\u5165\u53E3\u3001\u91CD\u90E8\u7F72\u540E\u5168 404\uFF1B
161
+ # \u5E26 hash \u7684 assets \u624D\u914D\u957F\u7F13\u5B58\uFF1B\u8DE8\u6E90\u90E8\u7F72\u65F6\u4FDD\u7559 Access-Control-Allow-Origin\uFF09
162
+ server {
163
+ listen ${listen};
164
+ root ${webRoot};
165
+
166
+ ${blocks}
167
+ }`;
168
+ }
169
+ function appSummary(app) {
170
+ const role = app.host ? app.remote ? "\u5BBF\u4E3B+\u8FDC\u7A0B" : "\u5BBF\u4E3B" : "\u8FDC\u7A0B";
171
+ const lines = [];
172
+ lines.push(` ${app.name}\uFF08${role}\uFF0C\u76EE\u5F55 ${app.path}\uFF0Cdev \u7AEF\u53E3 ${app.port}\uFF0Cbase ${app.base}\uFF09`);
173
+ if (app.host) {
174
+ for (const [k, v] of Object.entries(app.host.remotes)) {
175
+ lines.push(` \u6D88\u8D39\u8FDC\u7A0B ${k} \u2192 dev ${v.dev} / prod ${v.prod}`);
176
+ }
177
+ lines.push(` \u9875\u9762\u8DEF\u7531\u8868\uFF1A${app.host.pages.length} \u6761`);
178
+ }
179
+ if (app.remote?.exposes) {
180
+ lines.push(` exposes\uFF1A${Object.keys(app.remote.exposes).join("\u3001")}`);
181
+ }
182
+ if (app.remote?.remotes) {
183
+ for (const [k, v] of Object.entries(app.remote.remotes)) {
184
+ lines.push(` \u53CD\u5411\u6D88\u8D39 ${k} \u2192 dev ${v.dev} / prod ${v.prod}\uFF08\u53CC\u5411\u8054\u90A6\uFF0Cdev \u4E0B\u9700 devSharedSelf: true\uFF09`);
185
+ }
186
+ }
187
+ return lines.join("\n");
188
+ }
189
+ async function inspectConfig(configPath) {
190
+ const cfg = await loadFulgurConfig(configPath);
191
+ const out = [];
192
+ out.push(`${MARK} \u914D\u7F6E\u6821\u9A8C\u901A\u8FC7\uFF1Aroot=${cfg.root}\uFF0Capps=${cfg.apps.length}`);
193
+ out.push("\u5E94\u7528\u6458\u8981\uFF1A");
194
+ for (const app of cfg.apps) out.push(appSummary(app));
195
+ out.push("\n\u2500\u2500 \u5404\u5E94\u7528 vite.config.ts plugins \u7C98\u8D34\u5757 \u2500\u2500");
196
+ for (const app of cfg.apps) {
197
+ out.push(`
198
+ # ${app.path}\uFF08${app.name}\uFF09`);
199
+ out.push(viteSnippetFor(app));
200
+ }
201
+ out.push("\n\u2500\u2500 NGINX \u7AD9\u70B9\u6A21\u677F \u2500\u2500");
202
+ out.push(nginxSnippetFor(cfg));
203
+ out.push("\n\u2500\u2500 \u63A5\u5165\u6838\u5BF9\u6E05\u5355\uFF08\u8054\u90A6\u901A\u7528\u9879\uFF0C\u4E0E\u5177\u4F53\u9879\u76EE\u65E0\u5173\uFF09 \u2500\u2500");
204
+ out.push("1. \u5BBF\u4E3B\u4E0E\u8FDC\u7A0B\u90FD\u5B89\u88C5\u4F9D\u8D56\uFF1Apnpm add @fulgurjs/federation");
205
+ out.push("2. expose \u4E00\u5F8B\u6307\u5411\u72EC\u7ACB\u9875\uFF08\u9875\u9762\u4ECE\u8DEF\u7531\u53D6\u53C2\uFF09\uFF1B\u7EC4\u4EF6\u9700\u8981\u5FC5\u586B props \u65F6\u7ED9\u9ED8\u8BA4\u503C\uFF08BLD-003\uFF09");
206
+ out.push("3. shared \u91CC vue / vue-router / pinia \u5EFA\u8BAE singleton: true\u2014\u2014\u8DE8\u5E94\u7528\u5FC5\u987B\u540C\u5B9E\u4F8B\uFF08\u5168\u5C40\u54CD\u5E94\u6027\u3001getActivePinia\u3001\u8DEF\u7531\u6CE8\u5165\uFF09");
207
+ out.push("4. \u8FDC\u7A0B\u7684\u5168\u5C40\u526F\u4F5C\u7528\uFF08\u5168\u5C40\u7EC4\u4EF6/\u6307\u4EE4/\u542F\u52A8\u671F\u521D\u59CB\u5316\uFF09\u5C01\u88C5\u4E3A\u542F\u52A8\u5668\u6A21\u5757\u5E76 expose\uFF0C\u5BBF\u4E3B\u5728 loadRemote \u9875\u9762\u524D\u8C03\u7528\uFF1B");
208
+ out.push(" \u8DE8\u5E94\u7528\u5168\u5C40\u914D\u7F6E\uFF08locale/size \u7B49\uFF09\u7528\u8FD0\u884C\u65F6 provideFulgurAppConfig / getFulgurAppConfig \u534F\u5546\uFF0C\u4E0D\u8981\u5404\u526F\u672C\u624B\u5DE5\u6CE8\u5165");
209
+ out.push("5. \u8FDC\u7A0B\u9875\u9762\u53D6\u5BBF\u4E3B\u8FD0\u884C\u65F6\u4E00\u5F8B\u8D70 globalThis.__FULGUR_RUNTIME__ / getRuntime()\uFF0C\u7981\u6B62\u9759\u6001\u5BFC\u5165\u865A\u62DF\u8FD0\u884C\u65F6\uFF08DEV-008\uFF09");
210
+ out.push('6. dev \u51B7\u542F\u52A8\u9996\u8F6E 30~60s \u6709\u9884\u6784\u5EFA\u7A97\u53E3\uFF08\u77AC\u65F6 504/"ce"\uFF0CDEV-010\uFF09\uFF1A\u5148\u771F\u5B9E\u6253\u5F00\u9875\u9762\u9884\u70ED\u518D\u505A\u65AD\u8A00');
211
+ out.push("7. \u90E8\u7F72\u540E\u4F53\u68C0\uFF1Afulgur doctor --base <URL> --apps <\u5E94\u7528...>\uFF08\u7F13\u5B58\u5934/\u8D44\u6E90\u5F62\u6001/CORS/chunk \u53EF\u8FBE/\u7248\u672C skew\uFF09");
212
+ out.push("8. \u90E8\u7F72\u8BED\u4E49\uFF1AremoteEntry/manifest/index.html \u5FC5\u987B no-cache\uFF08\u4E25\u7981 immutable\uFF09\uFF1B\u5E26 hash \u7684 assets \u957F\u7F13\u5B58");
213
+ return out.join("\n");
214
+ }
215
+ var MARK, STARTER_CONFIG, DEFAULT_SHARED_BLOCK;
216
+ var init_init = __esm({
217
+ "src/init.ts"() {
218
+ "use strict";
219
+ init_config();
220
+ MARK = "[fulgur:init]";
221
+ STARTER_CONFIG = `// fulgur.config.ts \u2014\u2014 @fulgurjs/federation \u63A5\u5165\u914D\u7F6E\uFF08\u5355\u6587\u4EF6\u9A71\u52A8\uFF0C\u53EF\u5165\u5E93\u3001\u53EF\u590D\u8DD1\uFF09
222
+ // \u7528\u6CD5\uFF1Anpx fulgur init --config fulgur.config.ts \u6821\u9A8C\u914D\u7F6E\u5E76\u8F93\u51FA\u53EF\u7C98\u8D34\u6837\u677F\u4E0E\u6838\u5BF9\u6E05\u5355
223
+ // npx fulgur doctor --base http://<\u7AD9\u70B9> --apps <\u5E94\u7528\u76EE\u5F55\u540D...> \u90E8\u7F72\u4F53\u68C0
224
+ import { defineFulgurConfig } from '@fulgurjs/federation/config'
225
+
226
+ export default defineFulgurConfig({
227
+ // \u5DE5\u7A0B\u6839\u76EE\u5F55\uFF08monorepo \u6839\u6216\u5355\u5E94\u7528\u4ED3\u5E93\u6839\uFF09
228
+ root: process.cwd(),
229
+ apps: [
230
+ // \u2500\u2500 \u5BBF\u4E3B\uFF1A\u6D88\u8D39\u8FDC\u7A0B\u9875\u9762/\u7EC4\u4EF6\uFF08\u4E5F\u53EF\u518D expose \u81EA\u5DF1\u7684\u7EC4\u4EF6\u7ED9\u66F4\u9AD8\u5C42\u6D88\u8D39\uFF09\u2500\u2500
231
+ {
232
+ path: 'apps/host', // \u76F8\u5BF9 root \u7684\u5E94\u7528\u76EE\u5F55
233
+ name: 'host-app', // \u8054\u90A6\u5BB9\u5668\u540D\uFF08\u8FDC\u7A0B\u5F15\u7528\u5B83\u65F6\u7528\u8FD9\u4E2A\u540D\uFF09
234
+ port: 5173, // dev \u7AEF\u53E3
235
+ base: '/', // \u90E8\u7F72/dev \u7684 URL \u524D\u7F00
236
+ host: {
237
+ // \u8DEF\u7531\u524D\u7F00 \u2192 \u8FDC\u7A0B\u540D\uFF1A\u5BBF\u4E3B\u628A /remote-a/** \u7684\u9875\u9762\u89E3\u6790\u5230\u8FDC\u7A0B remote-a
238
+ remotePrefixes: { '/remote-a/': 'remote-a' },
239
+ // \u8FDC\u7A0B\u5730\u5740\uFF1A\u88F8 URL\uFF08\u5BF9\u8C61\u5F62\u5F0F\u4E0D\u652F\u6301 name@ \u524D\u7F00\uFF0C\u89C1\u9519\u8BEF\u7801 CFG-007\uFF09
240
+ remotes: {
241
+ 'remote-a': { dev: 'http://localhost:5174/remote-a', prod: '/remote-a' },
242
+ },
243
+ // \u9875\u9762\u8DEF\u7531\u8868\uFF1A\u5BBF\u4E3B\u8DEF\u5F84 \u2192 \u8FDC\u7A0B exposes \u952E\uFF08\u9ED8\u8BA4\u63A8\u5BFC pages/<\u53BB\u524D\u7F00\u53BB\u53C2\u6570\u6BB5>\uFF1B
244
+ // \u5E26\u53C2\u8DEF\u7531\u6216\u975E\u9ED8\u8BA4\u952E\u7528 spec \u663E\u5F0F\u8986\u76D6\uFF09
245
+ pages: [
246
+ { route: '/remote-a/home', name: 'RemoteAHome', title: '\u9996\u9875' },
247
+ { route: '/remote-a/detail/:id', name: 'RemoteADetail', spec: 'pages/remote-a/detail', title: '\u8BE6\u60C5' },
248
+ ],
249
+ },
250
+ },
251
+ // \u2500\u2500 \u8FDC\u7A0B\uFF1Aexpose \u72EC\u7ACB\u9875\uFF08\u9875\u9762\u81EA\u5DF1\u4ECE\u8DEF\u7531\u53D6\u53C2\uFF0C\u4E0D\u4F9D\u8D56\u7236\u7EC4\u4EF6\u4F20\u5FC5\u586B props\uFF0C\u89C1 BLD-003\uFF09\u2500\u2500
252
+ {
253
+ path: 'apps/remote-a',
254
+ name: 'remote-a',
255
+ port: 5174,
256
+ base: '/remote-a',
257
+ remote: {
258
+ exposes: {
259
+ './pages/remote-a/home': './src/views/Home.vue',
260
+ './pages/remote-a/detail': './src/views/Detail.vue',
261
+ },
262
+ },
263
+ },
264
+ ],
265
+ // \u90E8\u7F72\u4FE1\u606F\uFF08\u53EF\u9009\uFF09\uFF1A\u4EC5\u4F9B init \u8F93\u51FA NGINX \u6837\u677F\u65F6\u4F7F\u7528
266
+ deploy: { webRoot: '/var/www/your-site', listen: 8080 },
267
+ })
268
+ `;
269
+ DEFAULT_SHARED_BLOCK = `shared: {
270
+ vue: { singleton: true, requiredVersion: '^3.4.0' },
271
+ 'vue-router': { singleton: true, requiredVersion: '^4.4.5' },
272
+ pinia: { singleton: true, requiredVersion: '^2.1.7' },
273
+ }`;
274
+ }
275
+ });
276
+
277
+ // src/cli.ts
278
+ import { resolve } from "node:path";
279
+
280
+ // src/doctor.ts
281
+ import http from "node:http";
282
+ import https from "node:https";
283
+ function fetchHeadOrGet(url, method = "GET") {
284
+ return new Promise((resolve2, reject) => {
285
+ const mod = url.startsWith("https:") ? https : http;
286
+ const req = mod.request(url, { method, headers: { accept: "*/*" } }, (res) => {
287
+ const chunks = [];
288
+ res.on("data", (c) => {
289
+ if (chunks.reduce((n, b) => n + b.length, 0) < 2 * 1024 * 1024) chunks.push(c);
290
+ });
291
+ res.on(
292
+ "end",
293
+ () => resolve2({
294
+ status: res.statusCode ?? 0,
295
+ headers: res.headers,
296
+ body: Buffer.concat(chunks).toString("utf8")
297
+ })
298
+ );
299
+ });
300
+ req.on("error", reject);
301
+ req.setTimeout(8e3, () => req.destroy(new Error("timeout after 8000ms")));
302
+ req.end();
303
+ });
304
+ }
305
+ function header(res, name) {
306
+ const v = res.headers[name];
307
+ return Array.isArray(v) ? v[0] : v;
308
+ }
309
+ async function checkStatus(url, app, item, opts = {}) {
310
+ let res;
311
+ try {
312
+ res = await fetchHeadOrGet(url, opts.method ?? "GET");
313
+ } catch (e) {
314
+ return {
315
+ check: {
316
+ app,
317
+ item,
318
+ level: "FAIL",
319
+ symptom: `${item} \u4E0D\u53EF\u8FBE\uFF1A${url}\uFF08${String(e.message ?? e)}\uFF09`,
320
+ cause: "\u9759\u6001\u8D44\u6E90\u672A\u90E8\u7F72\u3001nginx \u7AD9\u70B9\u672A\u8D77\u6216\u8DEF\u5F84\u9519\u4F4D",
321
+ fix: "\u6838\u5BF9\u90E8\u7F72\u76EE\u5F55\uFF08rsync dist/{app}/\uFF09\u4E0E nginx location \u56DE\u9000\u89C4\u5219\uFF1Bcurl \u8BE5 URL \u786E\u8BA4\u53EF\u8FBE"
322
+ }
323
+ };
324
+ }
325
+ if (res.status !== 200) {
326
+ return {
327
+ check: {
328
+ app,
329
+ item,
330
+ level: "FAIL",
331
+ symptom: `${item} \u8FD4\u56DE ${res.status}\uFF1A${url}`,
332
+ cause: "\u6587\u4EF6\u7F3A\u5931\uFF08\u672A\u90E8\u7F72/\u5DF2\u88AB\u6E05\uFF09\u6216 nginx \u56DE\u9000\u5230\u4E86\u5BBF\u4E3B index.html\uFF08\u6DF1\u94FE\u56DE\u9000\u8FC7\u5BBD\uFF09",
333
+ fix: "\u91CD\u65B0\u90E8\u7F72\u8BE5\u5E94\u7528\u4EA7\u7269\uFF1B\u6838\u5BF9 nginx location \u7CBE\u786E\u5339\u914D remoteEntry \u7684\u89C4\u5219"
334
+ },
335
+ res
336
+ };
337
+ }
338
+ if (opts.expectJs) {
339
+ const head = res.body.slice(0, 200).trimStart();
340
+ if (head.startsWith("<")) {
341
+ return {
342
+ res,
343
+ check: {
344
+ app,
345
+ item,
346
+ level: "FAIL",
347
+ symptom: `${item} \u8FD4\u56DE\u7684\u662F HTML \u800C\u975E JS\uFF1A${url}`,
348
+ cause: "nginx try_files/\u6DF1\u94FE\u56DE\u9000\u628A\u5165\u53E3\u8BF7\u6C42\u515C\u5230\u4E86 index.html\uFF08\u8054\u90A6\u5BBF\u4E3B\u4F1A\u628A\u5B83\u5F53\u6A21\u5757\u89E3\u6790\u76F4\u63A5\u5931\u8D25\uFF09",
349
+ fix: "\u4E3A remoteEntry/\u5165\u53E3\u589E\u52A0\u7CBE\u786E location \u539F\u6837\u8FD4\u56DE JS"
350
+ }
351
+ };
352
+ }
353
+ }
354
+ if (opts.expectNoCache) {
355
+ const cc = header(res, "cache-control") ?? "";
356
+ if (/immutable/i.test(cc)) {
357
+ return {
358
+ res,
359
+ check: {
360
+ app,
361
+ item,
362
+ level: "FAIL",
363
+ symptom: `${item} \u7684 Cache-Control=${cc}\uFF08immutable\uFF09\uFF1A${url}`,
364
+ cause: "\u6587\u4EF6\u540D\u56FA\u5B9A\u800C\u5185\u5BB9\u6BCF\u6B21\u6784\u5EFA\u53D8\u5316\u2014\u2014\u6D4F\u89C8\u5668\u4F1A\u957F\u671F\u6301\u6709\u65E7\u7248\u672C\uFF0C\u91CD\u90E8\u7F72\u540E\u65E7 hash chunk \u88AB\u6E05\u7406\u5373\u5168 404\uFF082026-09-17 \u7528\u6237\u5B9E\u6D4B\u8E29\u5751\uFF09",
365
+ fix: 'nginx \u6539\u4E3A Cache-Control "no-cache"\uFF08\u534F\u5546\u7F13\u5B58\uFF09\uFF1B\u5E26 hash \u7684 assets \u624D\u914D\u957F\u7F13\u5B58'
366
+ }
367
+ };
368
+ }
369
+ if (!/no-cache|max-age=0/i.test(cc) && !/no-store/i.test(cc)) {
370
+ return {
371
+ res,
372
+ check: {
373
+ app,
374
+ item,
375
+ level: "WARN",
376
+ symptom: `${item} \u7F3A\u5C11 no-cache \u7F13\u5B58\u5934\uFF08Cache-Control=${cc || "\u65E0"}\uFF09\uFF1A${url}`,
377
+ cause: "\u6D4F\u89C8\u5668/\u4E2D\u95F4\u5C42\u53EF\u80FD\u6301\u6709\u65E7\u7248\u672C\uFF0C\u91CD\u90E8\u7F72\u540E\u884C\u4E3A\u4E0D\u53EF\u9884\u671F",
378
+ fix: 'nginx \u4E3A remoteEntry/manifest/index.html \u663E\u5F0F add_header Cache-Control "no-cache"'
379
+ }
380
+ };
381
+ }
382
+ }
383
+ return { res };
384
+ }
385
+ function extractChunkRefs(...sources) {
386
+ const refs = /* @__PURE__ */ new Set();
387
+ const add = (raw) => refs.add(raw.replace(/^\.\//, ""));
388
+ for (const body of sources) {
389
+ if (!body) continue;
390
+ for (const m of body.matchAll(/\.\/([\w./-]+\.js)/g)) add(m[1]);
391
+ for (const m of body.matchAll(/"([\w./-]+\.js)"/g)) add(m[1]);
392
+ for (const m of body.matchAll(/(?:src|href)="([\w./-]+\.js)"/g)) add(m[1]);
393
+ }
394
+ return [...refs];
395
+ }
396
+ function compareSharedVersions(perAppShared) {
397
+ const out = [];
398
+ const byKey = /* @__PURE__ */ new Map();
399
+ for (const { app, shared } of perAppShared) {
400
+ for (const s of shared ?? []) {
401
+ if (!byKey.has(s.name)) byKey.set(s.name, /* @__PURE__ */ new Map());
402
+ const byVer = byKey.get(s.name);
403
+ if (!byVer.has(s.version)) byVer.set(s.version, /* @__PURE__ */ new Set());
404
+ byVer.get(s.version).add(app);
405
+ }
406
+ }
407
+ for (const [key, byVer] of byKey) {
408
+ if (byVer.size <= 1) continue;
409
+ const dist = [...byVer.entries()].map(([v, apps]) => `${v}(${[...apps].join("/")})`).join(" vs ");
410
+ out.push({
411
+ app: "*",
412
+ item: `shared \u7248\u672C skew\uFF1A${key}`,
413
+ level: "WARN",
414
+ symptom: `\u5171\u4EAB\u952E "${key}" \u5728\u591A\u5E94\u7528\u95F4\u7248\u672C\u4E0D\u4E00\u81F4\uFF1A${dist}`,
415
+ cause: "\u975E singleton \u65F6\u9875\u9762\u5C06\u6309\u6700\u9AD8\u7248\u672C\u534F\u5546\uFF08\u5404\u5E94\u7528\u62FF\u5230\u7684\u5B9E\u4F8B\u53EF\u80FD\u4E0D\u540C\uFF09\uFF1Bsingleton \u65F6\u5168\u90E8\u6536\u655B\u5230\u5DF2\u52A0\u8F7D\u5B9E\u4F8B\u2014\u2014\u884C\u4E3A\u9700\u8981\u88AB\u77E5\u6653\u800C\u4E0D\u662F\u649E\u4E0A",
416
+ fix: "\u7EDF\u4E00\u5404\u5E94\u7528\u4F9D\u8D56\u7248\u672C\uFF1Bsingleton \u573A\u666F\u786E\u8BA4 skew \u544A\u8B66\uFF08MFU-010\uFF09\u53EF\u63A5\u53D7"
417
+ });
418
+ }
419
+ return out;
420
+ }
421
+ async function runDoctor(opts) {
422
+ const checks = [];
423
+ const perAppShared = [];
424
+ const chunkSample = opts.chunkSample ?? 16;
425
+ for (const app of opts.apps) {
426
+ const root = `${opts.base.replace(/\/$/, "")}/${app.replace(/^\//, "")}`;
427
+ const entry = await checkStatus(`${root}/fulgur-remoteEntry.js`, app, "fulgur-remoteEntry.js", {
428
+ expectJs: true,
429
+ expectNoCache: !opts.dev
430
+ });
431
+ if (entry.check) checks.push(entry.check);
432
+ if (entry.res && !opts.dev) {
433
+ const acao = header(entry.res, "access-control-allow-origin");
434
+ checks.push({
435
+ app,
436
+ item: "remoteEntry CORS",
437
+ level: acao ? "PASS" : "WARN",
438
+ symptom: acao ? `Access-Control-Allow-Origin: ${acao}` : "remoteEntry \u672A\u5E26 Access-Control-Allow-Origin",
439
+ cause: acao ? void 0 : "\u8DE8\u6E90\u8054\u90A6\uFF08dev \u53CC\u7AEF\u53E3/\u5F02\u57DF\u90E8\u7F72\uFF09\u4F1A\u88AB CORS \u62E6\u622A\uFF1B\u540C\u6E90\u90E8\u7F72\u4E0D\u53D7\u5F71\u54CD",
440
+ fix: acao ? void 0 : 'nginx \u5BF9 remoteEntry add_header Access-Control-Allow-Origin "*"'
441
+ });
442
+ }
443
+ const manifestRes = await checkStatus(`${root}/fulgur-manifest.json`, app, "fulgur-manifest.json", {
444
+ expectNoCache: !opts.dev
445
+ });
446
+ if (manifestRes.check) checks.push(manifestRes.check);
447
+ let manifest;
448
+ if (manifestRes.res?.status === 200) {
449
+ try {
450
+ manifest = JSON.parse(manifestRes.res.body);
451
+ perAppShared.push({ app, shared: manifest.shared ?? [] });
452
+ } catch {
453
+ checks.push({
454
+ app,
455
+ item: "manifest \u89E3\u6790",
456
+ level: "FAIL",
457
+ symptom: "fulgur-manifest.json \u4E0D\u662F\u5408\u6CD5 JSON",
458
+ cause: "\u4EA7\u7269\u4E0D\u5B8C\u6574\u6216\u88AB\u4E2D\u95F4\u5C42\u6539\u5199",
459
+ fix: "\u91CD\u65B0\u6784\u5EFA\u90E8\u7F72\uFF1B\u786E\u8BA4 nginx \u672A\u5BF9\u8BE5\u8DEF\u5F84\u505A sub/\u62FC\u63A5\u6539\u5199"
460
+ });
461
+ }
462
+ }
463
+ let htmlBody = "";
464
+ if (!opts.dev) {
465
+ const html = await checkStatus(`${root}/index.html`, app, "index.html", { expectNoCache: true });
466
+ if (html.check) checks.push(html.check);
467
+ htmlBody = html.res?.body ?? "";
468
+ }
469
+ if (opts.dev) {
470
+ const devEntry = await checkStatus(`${root}/@fulgur-entry.js`, app, "@fulgur-entry.js\uFF08dev \u5BB9\u5668\u5165\u53E3\uFF09", {
471
+ expectJs: true
472
+ });
473
+ if (devEntry.check) checks.push(devEntry.check);
474
+ }
475
+ if (!opts.dev && manifest) {
476
+ const manifestRefs = [];
477
+ const exposes = manifest.exposes ?? {};
478
+ for (const key of Object.keys(exposes)) {
479
+ const f = exposes[key]?.file;
480
+ if (f) manifestRefs.push(String(f).replace(/^\.\//, ""));
481
+ }
482
+ const htmlRefs = extractChunkRefs(htmlBody);
483
+ const entryRefs = extractChunkRefs(entry.res?.body ?? "");
484
+ const refs = [.../* @__PURE__ */ new Set([...htmlRefs, ...manifestRefs, ...entryRefs])];
485
+ const resolveUrl = (ref) => {
486
+ if (/^(https?:)?\/\//.test(ref)) return ref;
487
+ if (ref.startsWith("/")) return new URL(ref, opts.base).href;
488
+ return `${root}/${ref}`;
489
+ };
490
+ const maskedByFallback = (body) => body.trimStart().startsWith("<");
491
+ for (const ref of refs.slice(0, chunkSample)) {
492
+ const url = resolveUrl(ref);
493
+ try {
494
+ const r = await fetchHeadOrGet(url, "GET");
495
+ if (r.status !== 200) {
496
+ checks.push({
497
+ app,
498
+ item: `chunk \u53EF\u8FBE\uFF1A${ref}`,
499
+ level: "FAIL",
500
+ symptom: `\u5165\u53E3/manifest \u5F15\u7528\u7684 chunk \u8FD4\u56DE ${r.status}\uFF1A${url}`,
501
+ cause: "\u4EA7\u7269\u4E0D\u5B8C\u6574\u6216\u90E8\u7F72\u540E partial \u6E05\u7406\u2014\u2014\u6D4F\u89C8\u5668\u6301\u65E7 remoteEntry \u65F6\u5178\u578B\u75C7\u72B6\uFF08immutable \u5751\uFF09",
502
+ fix: "\u91CD\u65B0\u5B8C\u6574\u90E8\u7F72\u8BE5\u5E94\u7528\uFF08rsync -a --delete dist/<app>/\uFF09\uFF1B\u786E\u8BA4\u7F13\u5B58\u5934\u4E3A no-cache"
503
+ });
504
+ } else if (maskedByFallback(r.body)) {
505
+ checks.push({
506
+ app,
507
+ item: `chunk \u53EF\u8FBE\uFF1A${ref}`,
508
+ level: "FAIL",
509
+ symptom: `chunk \u8BF7\u6C42\u8FD4\u56DE 200 \u4F46\u5185\u5BB9\u662F HTML\uFF08nginx \u6DF1\u94FE\u56DE\u9000\u63A9\u76D6\u4E86\u8D44\u6E90\u7F3A\u5931\uFF09\uFF1A${url}`,
510
+ cause: "try_files \u56DE\u9000\u8FC7\u5BBD\uFF1A\u7F3A\u5931\u7684\u9759\u6001\u8D44\u6E90\u88AB\u515C\u5230\u5BBF\u4E3B index.html\uFF0C\u6D4F\u89C8\u5668\u628A\u5B83\u5F53\u6A21\u5757\u89E3\u6790\u76F4\u63A5\u62A5\u9519",
511
+ fix: "\u4E3A\u9759\u6001 assets \u76EE\u5F55\u589E\u52A0\u7CBE\u786E\u5339\u914D\uFF08try_files $uri =404 \u6216\u72EC\u7ACB location\uFF09\uFF0C\u907F\u514D JS \u8BF7\u6C42\u88AB\u56DE\u9000\u6210 HTML"
512
+ });
513
+ } else {
514
+ for (const sub of extractChunkRefs(r.body)) if (!refs.includes(sub)) refs.push(sub);
515
+ }
516
+ } catch (e) {
517
+ checks.push({
518
+ app,
519
+ item: `chunk \u53EF\u8FBE\uFF1A${ref}`,
520
+ level: "FAIL",
521
+ symptom: `chunk \u8BF7\u6C42\u5931\u8D25\uFF1A${url}\uFF08${String(e.message ?? e)}\uFF09`,
522
+ cause: "\u7F51\u7EDC/\u90E8\u7F72\u9762\u5F02\u5E38",
523
+ fix: "curl \u590D\u6838\u8BE5 URL\uFF1B\u6838\u5BF9\u90E8\u7F72\u5B8C\u6574\u6027"
524
+ });
525
+ }
526
+ }
527
+ for (const ref of refs.slice(chunkSample, chunkSample * 4)) {
528
+ const url = resolveUrl(ref);
529
+ try {
530
+ const r = await fetchHeadOrGet(url, "HEAD");
531
+ if (r.status !== 200 && r.status !== 404) {
532
+ checks.push({
533
+ app,
534
+ item: `chunk \u53EF\u8FBE\uFF1A${ref}`,
535
+ level: "FAIL",
536
+ symptom: `chunk \u5F15\u7528\u8FD4\u56DE ${r.status}\uFF1A${url}`,
537
+ cause: "\u4EA7\u7269\u4E0D\u5B8C\u6574\u6216\u90E8\u7F72\u540E partial \u6E05\u7406",
538
+ fix: "\u91CD\u65B0\u5B8C\u6574\u90E8\u7F72\u8BE5\u5E94\u7528\uFF08rsync -a --delete dist/<app>/\uFF09"
539
+ });
540
+ }
541
+ } catch {
542
+ }
543
+ }
544
+ }
545
+ }
546
+ checks.push(...compareSharedVersions(perAppShared));
547
+ return { checks, failed: checks.some((c) => c.level === "FAIL") };
548
+ }
549
+ function formatDoctorReport(checks) {
550
+ const lines = [];
551
+ for (const c of checks) {
552
+ const head = `[fulgur:doctor] ${c.level} [${c.app}] ${c.item}`;
553
+ lines.push(c.level === "PASS" ? `${head} \u2014 ${c.symptom}` : `${head}
554
+ \u73B0\u8C61\uFF1A${c.symptom}`);
555
+ if (c.cause) lines.push(` \u6839\u56E0\uFF1A${c.cause}`);
556
+ if (c.fix) lines.push(` \u4FEE\u6CD5\uFF1A${c.fix}`);
557
+ }
558
+ const fail = checks.filter((c) => c.level === "FAIL").length;
559
+ const warn = checks.filter((c) => c.level === "WARN").length;
560
+ const pass = checks.filter((c) => c.level === "PASS").length;
561
+ lines.push(`[fulgur:doctor] \u6C47\u603B\uFF1A${pass} PASS / ${warn} WARN / ${fail} FAIL`);
562
+ return lines.join("\n");
563
+ }
564
+
565
+ // src/cli.ts
566
+ var HELP = `fulgur \u2014 Vite Module Federation CLI (@fulgurjs/federation)
567
+
568
+ \u7528\u6CD5\uFF1A
569
+ fulgur init [--template <path>] [--force] \u751F\u6210\u5E26\u6CE8\u91CA\u7684 fulgur.config.ts \u8D77\u6B65\u6A21\u677F
570
+ fulgur init --config <path> \u6821\u9A8C\u914D\u7F6E\uFF1B\u8F93\u51FA\u5404\u5E94\u7528 federation() \u7C98\u8D34\u5757\u3001
571
+ NGINX no-cache \u7AD9\u70B9\u6A21\u677F\u4E0E\u63A5\u5165\u6838\u5BF9\u6E05\u5355
572
+ fulgur doctor --base <URL> --apps <a,b,c> [--dev] [--json] [--chunk-sample N]
573
+ fulgur --help
574
+
575
+ \u793A\u4F8B\uFF1A
576
+ fulgur init # \u5F53\u524D\u76EE\u5F55\u5199 fulgur.config.ts\uFF08\u5DF2\u5B58\u5728\u5219\u62D2\u7EDD\uFF0C--force \u8986\u76D6\uFF09
577
+ fulgur init --config fulgur.config.ts # \u6821\u9A8C + \u8F93\u51FA\u6837\u677F
578
+ fulgur doctor --base http://your-site --apps app-a,app-b
579
+ fulgur doctor --base http://localhost:5173 --apps app-a --dev
580
+ `;
581
+ async function main() {
582
+ const argv = process.argv.slice(2);
583
+ const cmd = argv[0];
584
+ if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
585
+ console.log(HELP);
586
+ return 0;
587
+ }
588
+ const argOf = (k) => {
589
+ const i = argv.indexOf(k);
590
+ return i > -1 ? argv[i + 1] : void 0;
591
+ };
592
+ const has = (k) => argv.includes(k);
593
+ if (cmd === "init") {
594
+ const { writeConfigTemplate: writeConfigTemplate2, inspectConfig: inspectConfig2 } = await Promise.resolve().then(() => (init_init(), init_exports));
595
+ const configPath = argOf("--config");
596
+ if (configPath) {
597
+ try {
598
+ console.log(await inspectConfig2(resolve(configPath)));
599
+ return 0;
600
+ } catch (e) {
601
+ console.error(String(e.message ?? e));
602
+ return 2;
603
+ }
604
+ }
605
+ const target = resolve(argOf("--template") ?? "fulgur.config.ts");
606
+ const result = await writeConfigTemplate2(target, has("--force"));
607
+ if (result === "exists") {
608
+ console.error(`[fulgur:init] ${target} \u5DF2\u5B58\u5728\uFF0C\u62D2\u7EDD\u8986\u76D6\uFF08--force \u5F3A\u5236\u8986\u76D6\uFF09`);
609
+ return 2;
610
+ }
611
+ console.log(`[fulgur:init] \u5DF2\u751F\u6210\u8D77\u6B65\u6A21\u677F ${target}
612
+ \u540E\u7EED\u6B65\u9AA4\uFF1A
613
+ 1. \u7F16\u8F91 fulgur.config.ts\uFF1A\u586B\u5165\u4F60\u7684\u5E94\u7528\u76EE\u5F55/\u5BB9\u5668\u540D/\u7AEF\u53E3/base/\u9875\u9762\u8DEF\u7531\u8868/exposes
614
+ 2. npx fulgur init --config fulgur.config.ts # \u6821\u9A8C\u5E76\u8F93\u51FA\u53EF\u7C98\u8D34\u6837\u677F\u4E0E\u6838\u5BF9\u6E05\u5355
615
+ 3. \u6309\u6E05\u5355\u628A federation() \u5757\u63A5\u5165\u5404\u5E94\u7528 vite.config.ts\uFF0C\u5B89\u88C5\u4F9D\u8D56
616
+ 4. \u90E8\u7F72\u540E\uFF1Anpx fulgur doctor --base <URL> --apps <\u5E94\u7528...>`);
617
+ return 0;
618
+ }
619
+ if (cmd === "doctor") {
620
+ const base = argOf("--base");
621
+ const appsRaw = argOf("--apps") ?? "main";
622
+ if (!base) {
623
+ console.error("[fulgur:doctor] \u7F3A\u5C11 --base <URL>\uFF08\u7AD9\u70B9\u6839\u5730\u5740\uFF0C\u5982 http://your-site\uFF09");
624
+ return 2;
625
+ }
626
+ const apps = appsRaw.split(",").map((s) => s.trim()).filter(Boolean);
627
+ const { checks, failed } = await runDoctor({
628
+ base,
629
+ apps,
630
+ dev: has("--dev"),
631
+ chunkSample: argOf("--chunk-sample") ? Number(argOf("--chunk-sample")) : void 0
632
+ });
633
+ if (has("--json")) {
634
+ console.log(JSON.stringify({ base, apps, dev: has("--dev"), checks, failed }, null, 2));
635
+ } else {
636
+ console.log(formatDoctorReport(checks));
637
+ }
638
+ return failed ? 1 : 0;
639
+ }
640
+ console.error(`\u672A\u77E5\u547D\u4EE4\uFF1A${cmd}
641
+ ${HELP}`);
642
+ return 2;
643
+ }
644
+ main().then(
645
+ (code) => process.exit(code),
646
+ (e) => {
647
+ console.error("[fulgur] \u6267\u884C\u5931\u8D25\uFF1A", e);
648
+ process.exit(2);
649
+ }
650
+ );