@fulgurjs/federation 3.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -9,30 +9,185 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
+ // src/manifest.ts
13
+ function validAssetRef(v) {
14
+ if (typeof v !== "string" || v.length === 0) return false;
15
+ if (/[\s\u0000-\u001f]/.test(v)) return false;
16
+ if (/^https?:\/\/[^/@\s]+:[^/@\s]+@/.test(v)) return false;
17
+ return true;
18
+ }
19
+ function validExposeName(v) {
20
+ return typeof v === "string" && /^\.[\w./-]+$/.test(v);
21
+ }
22
+ function parseManifest(input) {
23
+ const issues = [];
24
+ if (!isPlainObject(input)) {
25
+ return { issues: [{ field: "$", message: "manifest \u5FC5\u987B\u662F JSON \u5BF9\u8C61", got: typeof input }] };
26
+ }
27
+ const m = input;
28
+ let schemaVersion = MANIFEST_SCHEMA_VERSION;
29
+ if (m.schemaVersion !== void 0) {
30
+ if (typeof m.schemaVersion !== "number" || !Number.isInteger(m.schemaVersion)) {
31
+ issues.push({ field: "schemaVersion", message: "\u5FC5\u987B\u662F\u6574\u6570", got: m.schemaVersion });
32
+ } else if (m.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
33
+ return {
34
+ issues: [
35
+ {
36
+ field: "schemaVersion",
37
+ message: `\u4E0D\u652F\u6301\u7684 manifest \u4E3B\u7248\u672C\uFF08\u5F53\u524D\u6D88\u8D39\u7AEF\u652F\u6301 ${MANIFEST_SCHEMA_VERSION}\uFF09`,
38
+ got: m.schemaVersion
39
+ }
40
+ ],
41
+ unsupportedVersion: m.schemaVersion
42
+ };
43
+ }
44
+ }
45
+ if (typeof m.name !== "string" || m.name.length === 0) {
46
+ issues.push({ field: "name", message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: m.name });
47
+ }
48
+ const exposes = m.exposes;
49
+ const shared = m.shared;
50
+ const sharedIssues = validateShared(shared);
51
+ issues.push(...sharedIssues);
52
+ if (Array.isArray(exposes)) {
53
+ if (m.devServer !== true) {
54
+ issues.push({ field: "devServer", message: "dev \u6570\u7EC4\u5F62\u6001 exposes \u5FC5\u987B\u4F34\u968F devServer: true", got: m.devServer });
55
+ }
56
+ if (typeof m.base !== "string" || !/^\/(?:.*\/)?$/.test(m.base)) {
57
+ issues.push({ field: "base", message: "\u5FC5\u987B\u662F\u4EE5 / \u5F00\u5934\u4E14 / \u7ED3\u5C3E\u7684\u8DEF\u5F84", got: m.base });
58
+ }
59
+ if (!validAssetRef(m.entry)) {
60
+ issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740", got: m.entry });
61
+ }
62
+ for (const [i, e] of exposes.entries()) {
63
+ if (!isPlainObject(e)) {
64
+ issues.push({ field: `exposes[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof e });
65
+ continue;
66
+ }
67
+ if (!validExposeName(e.name)) {
68
+ issues.push({ field: `exposes[${i}].name`, message: '\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D', got: e.name });
69
+ }
70
+ if (typeof e.src !== "string" || e.src.length === 0) {
71
+ issues.push({ field: `exposes[${i}].src`, message: "dev \u5F62\u6001\u5FC5\u987B\u63D0\u4F9B\u6E90\u7801\u76F8\u5BF9\u8DEF\u5F84", got: e.src });
72
+ }
73
+ if (!validAssetRef(e.file)) {
74
+ issues.push({ field: `exposes[${i}].file`, message: "\u5FC5\u987B\u662F\u53EF\u8BF7\u6C42\u7684\u6A21\u5757\u5730\u5740", got: e.file });
75
+ }
76
+ }
77
+ } else if (isPlainObject(exposes)) {
78
+ if (!validAssetRef(m.entry)) {
79
+ issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740\uFF08\u76F8\u5BF9\u57FA\u51C6 = \u6240\u5728\u76EE\u5F55\uFF09", got: m.entry });
80
+ }
81
+ for (const [key, v] of Object.entries(exposes)) {
82
+ if (!validExposeName(key)) {
83
+ issues.push({ field: `exposes["${key}"]`, message: '\u952E\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D' });
84
+ }
85
+ if (!isPlainObject(v)) {
86
+ issues.push({ field: `exposes["${key}"]`, message: "\u503C\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof v });
87
+ continue;
88
+ }
89
+ if (!validAssetRef(v.file)) {
90
+ issues.push({ field: `exposes["${key}"].file`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84\uFF08\u76F8\u5BF9 entry \u76EE\u5F55\uFF09", got: v.file });
91
+ }
92
+ if (v.css !== void 0) {
93
+ if (!Array.isArray(v.css)) {
94
+ issues.push({ field: `exposes["${key}"].css`, message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4", got: typeof v.css });
95
+ } else {
96
+ v.css.forEach((css, i) => {
97
+ if (!validAssetRef(css)) {
98
+ issues.push({ field: `exposes["${key}"].css[${i}]`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84", got: css });
99
+ }
100
+ });
101
+ }
102
+ }
103
+ }
104
+ } else {
105
+ issues.push({ field: "exposes", message: "\u5FC5\u987B\u662F\u6570\u7EC4\uFF08dev\uFF09\u6216\u5BF9\u8C61\uFF08prod\uFF09", got: typeof exposes });
106
+ }
107
+ if (issues.length > 0) return { issues };
108
+ return { manifest: input, issues };
109
+ }
110
+ function validateShared(shared) {
111
+ if (shared === void 0) return [];
112
+ const issues = [];
113
+ if (!Array.isArray(shared)) {
114
+ return [{ field: "shared", message: "\u5FC5\u987B\u662F\u6570\u7EC4", got: typeof shared }];
115
+ }
116
+ for (const [i, s] of shared.entries()) {
117
+ if (!isPlainObject(s)) {
118
+ issues.push({ field: `shared[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof s });
119
+ continue;
120
+ }
121
+ if (typeof s.name !== "string" || s.name.length === 0) {
122
+ issues.push({ field: `shared[${i}].name`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.name });
123
+ }
124
+ if (typeof s.version !== "string" || s.version.length === 0) {
125
+ issues.push({ field: `shared[${i}].version`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.version });
126
+ }
127
+ }
128
+ return issues;
129
+ }
130
+ function isDevManifest(m) {
131
+ return Array.isArray(m.exposes);
132
+ }
133
+ function normalizeExposes(manifest) {
134
+ const out = /* @__PURE__ */ new Map();
135
+ if (isDevManifest(manifest)) {
136
+ for (const e of manifest.exposes) {
137
+ if (!validExposeName(e.name)) continue;
138
+ out.set(e.name, { name: e.name, file: e.file, css: [], src: e.src });
139
+ }
140
+ } else {
141
+ for (const [key, v] of Object.entries(manifest.exposes)) {
142
+ if (!validExposeName(key)) continue;
143
+ out.set(key, { name: key, file: v.file, css: Array.isArray(v.css) ? v.css.filter((c) => typeof c === "string") : [] });
144
+ }
145
+ }
146
+ return out;
147
+ }
148
+ var MANIFEST_SCHEMA_VERSION, isPlainObject;
149
+ var init_manifest = __esm({
150
+ "src/manifest.ts"() {
151
+ "use strict";
152
+ MANIFEST_SCHEMA_VERSION = 1;
153
+ isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
154
+ }
155
+ });
156
+
12
157
  // src/config.ts
13
158
  async function loadRepoConfig(configPath) {
14
159
  const { pathToFileURL, fileURLToPath } = await import("node:url");
15
160
  const { createRequire } = await import("node:module");
16
- const fs2 = await import("node:fs");
161
+ const fs3 = await import("node:fs");
17
162
  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 : "@fulgurjs-federation-config-unavailable";
163
+ const path3 = await import("node:path");
164
+ const selfDir = await (async () => {
165
+ try {
166
+ return path3.dirname(fileURLToPath(import.meta.url));
167
+ } catch {
168
+ }
169
+ try {
170
+ return __dirname;
171
+ } catch {
172
+ }
173
+ return "";
174
+ })();
175
+ const selfConfigJs = selfDir ? path3.join(selfDir, "config.js") : "";
176
+ const SELF_SPEC = selfConfigJs && fs3.existsSync(selfConfigJs) ? pathToFileURL(selfConfigJs).href : "@fulgurjs-federation-config-unavailable";
22
177
  const rewriteSelfImports = (source) => source.replace(/(['"])@fulgurjs\/federation\/config\1/g, (_m, q) => `${q}${SELF_SPEC}${q}`);
23
178
  const importConfigs = async (cacheKey) => {
24
179
  return await import(`${cacheKey}?t=${Date.now()}`);
25
180
  };
26
181
  let mod;
27
182
  if (configPath.endsWith(".json")) {
28
- mod = { default: JSON.parse(fs2.readFileSync(configPath, "utf8")) };
183
+ mod = { default: JSON.parse(fs3.readFileSync(configPath, "utf8")) };
29
184
  } else {
30
- const source = rewriteSelfImports(fs2.readFileSync(configPath, "utf8"));
31
- const tmpBase = path2.join(os.tmpdir(), `fulgurjs-config-${Date.now()}`);
185
+ const source = rewriteSelfImports(fs3.readFileSync(configPath, "utf8"));
186
+ const tmpBase = path3.join(os.tmpdir(), `fulgurjs-config-${Date.now()}`);
32
187
  if (/\.(ts|mts|cts)$/.test(configPath)) {
33
188
  try {
34
189
  const tmpTs = `${tmpBase}.ts`;
35
- fs2.writeFileSync(tmpTs, source);
190
+ fs3.writeFileSync(tmpTs, source);
36
191
  mod = await importConfigs(pathToFileURL(tmpTs).href);
37
192
  } catch (e) {
38
193
  if (!/Unexpected|SyntaxError|ERR_UNSUPPORTED_NODE_MODULES|ERR_UNKNOWN_FILE_EXTENSION|ERR_IMPORT_ATTRIBUTE/.test(String(e)) && process.version.startsWith("v2")) {
@@ -48,12 +203,12 @@ async function loadRepoConfig(configPath) {
48
203
  }
49
204
  const out = esbuild.buildSync({ entryPoints: [configPath], bundle: false, write: false, format: "esm", target: "node18" });
50
205
  const jsFile = `${tmpBase}.mjs`;
51
- fs2.writeFileSync(jsFile, rewriteSelfImports(out.outputFiles[0].text));
206
+ fs3.writeFileSync(jsFile, rewriteSelfImports(out.outputFiles[0].text));
52
207
  mod = await importConfigs(pathToFileURL(jsFile).href);
53
208
  }
54
209
  } else {
55
210
  const jsFile = `${tmpBase}.mjs`;
56
- fs2.writeFileSync(jsFile, source);
211
+ fs3.writeFileSync(jsFile, source);
57
212
  mod = await importConfigs(pathToFileURL(jsFile).href);
58
213
  }
59
214
  }
@@ -73,12 +228,59 @@ async function loadRepoConfig(configPath) {
73
228
  throw new Error(
74
229
  `[fulgurjs:init] \u5E94\u7528 "${app.path}" \u65E2\u65E0 host \u4E5F\u65E0 remote \u89D2\u8272
75
230
  \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 }`
231
+ \u4FEE\u6CD5\uFF1A\u5BBF\u4E3B\u8865 host: { remotePrefixes, remotes }\uFF08pages \u53EF\u9009\uFF09\uFF0C\u8FDC\u7A0B\u8865 remote: { exposes }`
77
232
  );
78
233
  }
79
234
  }
80
235
  return cfg;
81
236
  }
237
+ function federationOptionsForApp(config, appPathOrName) {
238
+ const app = config.apps.find(
239
+ (a) => a.path === appPathOrName || a.name === appPathOrName
240
+ );
241
+ if (!app) {
242
+ throw new Error(
243
+ `[fulgurjs] \u5E94\u7528 "${appPathOrName}" \u4E0D\u5728\u4ED3\u5E93\u914D\u7F6E\u4E2D
244
+ \u53EF\u7528\u5E94\u7528: ${config.apps.map((a) => `${a.path}(${a.name})`).join("\u3001")}
245
+ \u4FEE\u6CD5: \u4FEE\u6B63\u5E94\u7528\u76EE\u5F55\u540D\u6216\u5BB9\u5668\u540D\uFF08fulgurjs.config.ts apps \u6570\u7EC4\u5185\u4E8C\u9009\u4E00\u5339\u914D\uFF09`
246
+ );
247
+ }
248
+ if (!app.host && !app.remote) {
249
+ throw new Error(
250
+ `[fulgurjs] \u5E94\u7528 "${app.path}" \u65E2\u65E0 host \u4E5F\u65E0 remote \u89D2\u8272
251
+ \u4FEE\u6CD5: \u5BBF\u4E3B\u8865 host: { remotePrefixes, remotes }\uFF0C\u8FDC\u7A0B\u8865 remote: { exposes }`
252
+ );
253
+ }
254
+ const remotes = {};
255
+ const remoteSource = /* @__PURE__ */ new Map();
256
+ const mergeRemotes = (from, role) => {
257
+ for (const [key, addr] of Object.entries(from ?? {})) {
258
+ const prev = remotes[key];
259
+ if (prev && (prev.dev !== addr.dev || prev.prod !== addr.prod)) {
260
+ throw new Error(
261
+ `[fulgurjs] \u5E94\u7528 "${app.path}" \u7684\u8FDC\u7A0B "${key}" \u5728 ${role} \u4E0E ${remoteSource.get(key)} \u4E24\u5904\u5730\u5740\u4E0D\u4E00\u81F4
262
+ ${remoteSource.get(key)}: dev=${prev.dev} prod=${prev.prod}
263
+ ${role}: dev=${addr.dev} prod=${addr.prod}
264
+ \u4FEE\u6CD5: \u4FEE\u6B63 fulgurjs.config.ts \u4F7F\u4E24\u5904\u4E00\u81F4\uFF08\u540C\u4E00\u8FDC\u7A0B\u5728\u4E00\u4E2A\u5E94\u7528\u5185\u53EA\u80FD\u6709\u4E00\u4E2A\u5730\u5740\uFF09`
265
+ );
266
+ }
267
+ remotes[key] = addr;
268
+ remoteSource.set(key, role);
269
+ }
270
+ };
271
+ mergeRemotes(app.host?.remotes, "host.remotes");
272
+ mergeRemotes(app.remote?.remotes, "remote.remotes");
273
+ const hasExposes = !!app.remote && Object.keys(app.remote.exposes).length > 0;
274
+ const hasRemotes = Object.keys(remotes).length > 0;
275
+ return {
276
+ name: app.name,
277
+ ...hasRemotes ? { remotes } : {},
278
+ ...hasExposes ? { exposes: app.remote.exposes } : {},
279
+ ...app.remote?.setup ? { setup: app.remote.setup } : {},
280
+ ...app.shared ? { shared: app.shared } : {},
281
+ devSharedSelf: app.devSharedSelf ?? (hasExposes || !!app.remote?.setup)
282
+ };
283
+ }
82
284
  var init_config = __esm({
83
285
  "src/config.ts"() {
84
286
  "use strict";
@@ -118,6 +320,8 @@ ${hostRemotes || remoteRemotes}
118
320
  const exposesBlock = app.remote?.exposes ? ` exposes: {
119
321
  ${exposesLiteral(app.remote.exposes)}
120
322
  },
323
+ ` : "";
324
+ const setupBlock = app.remote?.setup ? ` setup: '${app.remote.setup}',
121
325
  ` : "";
122
326
  const sharedBlock = app.shared ? ` shared: {
123
327
  ${Object.entries(app.shared).map(([k, v]) => {
@@ -129,7 +333,7 @@ ${Object.entries(app.shared).map(([k, v]) => {
129
333
  `;
130
334
  return `federation({
131
335
  name: '${app.name}',
132
- ${remotesBlock}${exposesBlock}${sharedBlock}})`;
336
+ ${remotesBlock}${exposesBlock}${setupBlock}${sharedBlock}})`;
133
337
  }
134
338
  function nginxSnippetFor(cfg) {
135
339
  const listen = cfg.deploy?.listen ?? 8080;
@@ -174,14 +378,17 @@ function appSummary(app) {
174
378
  for (const [k, v] of Object.entries(app.host.remotes)) {
175
379
  lines.push(` \u6D88\u8D39\u8FDC\u7A0B ${k} \u2192 dev ${v.dev} / prod ${v.prod}`);
176
380
  }
177
- lines.push(` \u9875\u9762\u8DEF\u7531\u8868\uFF1A${app.host.pages.length} \u6761`);
381
+ lines.push(` \u9875\u9762\u8DEF\u7531\u8868\uFF1A${app.host.pages?.length ?? 0} \u6761${app.host.pages ? "" : "\uFF08\u672A\u5728\u4ED3\u5E93\u914D\u7F6E\u4E2D\u63D0\u4F9B\u2014\u2014\u4EE5\u5E94\u7528\u4EE3\u7801\u9875\u9762\u8868\u4E3A\u51C6\uFF09"}`);
178
382
  }
179
383
  if (app.remote?.exposes) {
180
384
  lines.push(` exposes\uFF1A${Object.keys(app.remote.exposes).join("\u3001")}`);
181
385
  }
386
+ if (app.remote?.setup) {
387
+ lines.push(` setup\uFF08\u53EF\u9009\u8FDC\u7A0B\u521D\u59CB\u5316\uFF09\uFF1A${app.remote.setup}`);
388
+ }
182
389
  if (app.remote?.remotes) {
183
390
  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`);
391
+ lines.push(` \u53CD\u5411\u6D88\u8D39 ${k} \u2192 dev ${v.dev} / prod ${v.prod}\uFF08\u53CC\u5411\u8054\u90A6\uFF09`);
185
392
  }
186
393
  }
187
394
  return lines.join("\n");
@@ -204,11 +411,15 @@ async function inspectConfig(configPath) {
204
411
  out.push("1. \u5BBF\u4E3B\u4E0E\u8FDC\u7A0B\u90FD\u5B89\u88C5\u4F9D\u8D56\uFF1Apnpm add @fulgurjs/federation");
205
412
  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
413
  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\u4F20\u503C\uFF08locale/store/\u4E8B\u4EF6\u7B49\uFF09\u7EDF\u4E00\u8D70 virtual:fulgurjs-api \u7684 context \u51FD\u6570\uFF1A\u5BBF\u4E3B provideAppContext \u4E00\u6B21\u5199\u5165\uFF0C\u8FDC\u7A0B boot \u7528 getAppContext / requireAppContext \u6D88\u8D39");
209
- out.push("5. \u5E94\u7528\u4EE3\u7801\u552F\u4E00 API \u5165\u53E3\uFF1Aimport { loadRemote, provideAppContext, getAppContext, definePages, remoteSchema, remoteComponent } from 'virtual:fulgurjs-api'\u2014\u20143.0.0 \u8D77\u65E7\u5165\u53E3\uFF08virtual:fulgurjs-runtime / @fulgurjs/federation \u7684 context/pages/vue \u5B50\u8DEF\u5F84\uFF09\u5DF2\u5220\u9664\uFF0C\u4E00\u5F8B\u6539\u7528\u672C\u5165\u53E3");
414
+ out.push("4. \u8FDC\u7A0B\u9700\u8981\u542F\u52A8\u671F\u521D\u59CB\u5316\uFF08\u5168\u5C40\u7EC4\u4EF6/\u6837\u5F0F/locale \u7B49\uFF09\u65F6\uFF0C\u5728 federation({ setup }) \u58F0\u660E\u5165\u53E3\u6587\u4EF6\uFF1A");
415
+ out.push(" \u9ED8\u8BA4\u5BFC\u51FA setup(context) \u5E94\u7528\u7EA7\u6267\u884C\u4E00\u6B21\uFF08\u5BB9\u5668\u9996\u6B21\u88AB\u52A0\u8F7D\u4E1A\u52A1\u6A21\u5757\u524D\uFF09\uFF1B\u53EF\u9009\u5177\u540D\u5BFC\u51FA onSession(context)");
416
+ out.push(" \u6309\u5BBF\u4E3B sessionKey \u53BB\u91CD\u6267\u884C\uFF08\u6362\u8D26\u53F7/\u91CD\u767B\u81EA\u52A8\u91CD\u8DD1\uFF09\u3002\u5BBF\u4E3B\u65E0\u9700\u518D\u624B\u5199\u300CloadRemote \u542F\u52A8\u5668\u5E76\u8C03\u7528\u300D\uFF1B");
417
+ out.push(" \u8DE8\u5E94\u7528\u4F20\u503C\u7EDF\u4E00\u8D70 @fulgurjs/federation/runtime \u7684 context \u51FD\u6570\uFF1A\u5BBF\u4E3B provideAppContext \u5199\u5165");
418
+ out.push(" \uFF08\u9000\u51FA\u65F6 clearAppContext \u6E05\u7406\uFF09\uFF0C\u8FDC\u7A0B setup/onSession \u7528 getAppContext / requireAppContext \u6D88\u8D39");
419
+ out.push("5. \u5E94\u7528\u4EE3\u7801\u552F\u4E00 API \u5165\u53E3\uFF1Aimport { loadRemote, provideAppContext, getAppContext, clearAppContext, definePages, createHostPages, remoteSchema, remoteComponent } from '@fulgurjs/federation/runtime'");
210
420
  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\uFF1Afulgurjs doctor --base <URL> --apps <\u5E94\u7528...>\uFF08\u7F13\u5B58\u5934/\u8D44\u6E90\u5F62\u6001/CORS/chunk \u53EF\u8FBE/\u7248\u672C skew\uFF09");
421
+ out.push("7. \u90E8\u7F72\u540E\u4F53\u68C0\uFF1Afulgurjs doctor --base <URL> --apps <\u5E94\u7528...>\uFF08\u7F13\u5B58\u5934/\u8D44\u6E90\u5F62\u6001/CORS/chunk \u53EF\u8FBE/\u7248\u672C skew\uFF09\uFF1B");
422
+ out.push(" \u914D\u7F6E\u89E3\u91CA\uFF1Afulgurjs explain --config fulgurjs.config.ts --app <\u5E94\u7528>\uFF1B\u9875\u9762\u5951\u7EA6\u6838\u5BF9\uFF1Afulgurjs check-pages --config <...> --app <\u5BBF\u4E3B>");
212
423
  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
424
  return out.join("\n");
214
425
  }
@@ -259,6 +470,10 @@ export default defineRepoConfig({
259
470
  './pages/remote-a/home': './src/views/Home.vue',
260
471
  './pages/remote-a/detail': './src/views/Detail.vue',
261
472
  },
473
+ // \u53EF\u9009\uFF1A\u8FDC\u7A0B\u9700\u8981\u542F\u52A8\u671F\u521D\u59CB\u5316\uFF08\u5168\u5C40\u7EC4\u4EF6/\u6837\u5F0F/locale\uFF09\u65F6\u58F0\u660E\u5165\u53E3\u6587\u4EF6\u2014\u2014
474
+ // \u9ED8\u8BA4\u5BFC\u51FA setup(context) \u5E94\u7528\u7EA7\u6267\u884C\u4E00\u6B21\uFF1B\u53EF\u9009\u5177\u540D\u5BFC\u51FA onSession(context)
475
+ // \u6309\u5BBF\u4E3B sessionKey \u53BB\u91CD\u6267\u884C\u3002\u7F3A\u7701 = \u65E0\u521D\u59CB\u5316\u884C\u4E3A\uFF08\u666E\u901A expose \u8BED\u4E49\u4E0D\u53D8\uFF09
476
+ // setup: './src/fulgurjs/setup.ts',
262
477
  },
263
478
  },
264
479
  ],
@@ -274,135 +489,346 @@ export default defineRepoConfig({
274
489
  }
275
490
  });
276
491
 
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
-
284
- // src/manifest.ts
285
- var MANIFEST_SCHEMA_VERSION = 1;
286
- var isPlainObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
287
- function validAssetRef(v) {
288
- if (typeof v !== "string" || v.length === 0) return false;
289
- if (/[\s\u0000-\u001f]/.test(v)) return false;
290
- if (/^https?:\/\/[^/@\s]+:[^/@\s]+@/.test(v)) return false;
291
- return true;
292
- }
293
- function validExposeName(v) {
294
- return typeof v === "string" && /^\.[\w./-]+$/.test(v);
492
+ // src/pages.ts
493
+ function defaultDeriveSpec(route) {
494
+ return segs(route).slice(1).filter((seg) => !seg.startsWith(":")).join("/");
295
495
  }
296
- function parseManifest(input) {
297
- const issues = [];
298
- if (!isPlainObject(input)) {
299
- return { issues: [{ field: "$", message: "manifest \u5FC5\u987B\u662F JSON \u5BF9\u8C61", got: typeof input }] };
300
- }
301
- const m = input;
302
- let schemaVersion = MANIFEST_SCHEMA_VERSION;
303
- if (m.schemaVersion !== void 0) {
304
- if (typeof m.schemaVersion !== "number" || !Number.isInteger(m.schemaVersion)) {
305
- issues.push({ field: "schemaVersion", message: "\u5FC5\u987B\u662F\u6574\u6570", got: m.schemaVersion });
306
- } else if (m.schemaVersion !== MANIFEST_SCHEMA_VERSION) {
307
- return {
308
- issues: [
309
- {
310
- field: "schemaVersion",
311
- message: `\u4E0D\u652F\u6301\u7684 manifest \u4E3B\u7248\u672C\uFF08\u5F53\u524D\u6D88\u8D39\u7AEF\u652F\u6301 ${MANIFEST_SCHEMA_VERSION}\uFF09`,
312
- got: m.schemaVersion
313
- }
314
- ],
315
- unsupportedVersion: m.schemaVersion
316
- };
496
+ function shadows(earlier, later) {
497
+ const a = segs(earlier);
498
+ const b = segs(later);
499
+ if (a.length !== b.length) return null;
500
+ if (earlier === later) return "full";
501
+ let hasParam2 = false;
502
+ for (let i = 0; i < a.length; i++) {
503
+ if (a[i] === b[i]) continue;
504
+ if (a[i].startsWith(":")) {
505
+ hasParam2 = true;
506
+ continue;
317
507
  }
508
+ return null;
318
509
  }
319
- if (typeof m.name !== "string" || m.name.length === 0) {
320
- issues.push({ field: "name", message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: m.name });
510
+ return hasParam2 ? "param" : null;
511
+ }
512
+ function remoteOfRoute(route, remotes) {
513
+ if (!remotes) return null;
514
+ for (const [prefix, name] of Object.entries(remotes)) {
515
+ if (route === prefix.replace(/\/+$/, "") || route.startsWith(prefix)) return name;
321
516
  }
322
- const exposes = m.exposes;
323
- const shared = m.shared;
324
- const sharedIssues = validateShared(shared);
325
- issues.push(...sharedIssues);
326
- if (Array.isArray(exposes)) {
327
- if (m.devServer !== true) {
328
- issues.push({ field: "devServer", message: "dev \u6570\u7EC4\u5F62\u6001 exposes \u5FC5\u987B\u4F34\u968F devServer: true", got: m.devServer });
517
+ return null;
518
+ }
519
+ function validatePages(pages, options = {}) {
520
+ const deriveSpec = options.deriveSpec ?? defaultDeriveSpec;
521
+ const violations = [];
522
+ const effective = pages.map((p) => normSpec(p.spec ?? deriveSpec(p.route)));
523
+ pages.forEach((p, i) => {
524
+ if (p.spec || !hasParam(p.route)) return;
525
+ pages.forEach((q, j) => {
526
+ if (i === j) return;
527
+ if (effective[j] !== effective[i]) return;
528
+ violations.push({
529
+ rule: "R1",
530
+ level: "error",
531
+ message: `[R1] \u8DEF\u7531 "${p.route}" \u5265\u53C2\u63A8\u5BFC spec "${effective[i]}" \u4E0E\u8DEF\u7531 "${q.route}"${q.spec ? "\uFF08\u663E\u5F0F spec\uFF09" : ""}\u6536\u655B\u76F8\u540C\u3002
532
+ \u6839\u56E0\uFF1A\u53C2\u6570\u6BB5\u5265\u79BB\u540E expose \u952E\u4E0E\u5176\u5B83\u6761\u76EE\u76F8\u540C\uFF0C\u8BE5\u8DEF\u7531\u4F1A\u9759\u9ED8\u52A0\u8F7D\u9519\u8BEF\u7EC4\u4EF6
533
+ \uFF08\u771F\u5B9E\u4E8B\u6545\uFF1A\u5E26\u53C2\u52A8\u4F5C\u9875\u8BEF\u8D70\u5217\u8868/\u66F4\u65B0\u8BED\u4E49\uFF0C\u6570\u636E\u98CE\u9669\uFF09\u3002
534
+ \u4FEE\u6CD5\uFF1A\u4E3A\u8BE5\u6761\u76EE\u663E\u5F0F\u58F0\u660E spec: '...'\uFF08\u6307\u5411\u5E26\u53C2\u9875\u9762\u81EA\u5DF1\u7684 expose\uFF09\uFF0C\u6216\u8C03\u6574\u8DEF\u7531\u62C6\u5206\u3002`
535
+ });
536
+ });
537
+ });
538
+ const seenSpec = /* @__PURE__ */ new Map();
539
+ pages.forEach((p, i) => {
540
+ const key = effective[i];
541
+ if (seenSpec.has(key)) {
542
+ const first = pages[seenSpec.get(key)];
543
+ const bothExplicit = !!p.spec && !!first.spec;
544
+ violations.push({
545
+ rule: "R2",
546
+ level: "warn",
547
+ message: `[R2] \u8DEF\u7531 "${p.route}" \u4E0E "${first.route}" \u6709\u6548 spec \u76F8\u540C\uFF08"${key}"\uFF09\u3002${bothExplicit ? "\u4E24\u6761\u5747\u4E3A\u663E\u5F0F spec" : "\u5176\u4E2D\u542B\u63A8\u5BFC spec"}\u2014\u2014\u82E5\u4E3A\u523B\u610F\u7684\u83DC\u5355\u522B\u540D\u53EF\u5FFD\u7565\u672C\u8B66\u544A\uFF0C\u5426\u5219\u8BF7\u6838\u5BF9\u3002`
548
+ });
549
+ } else {
550
+ seenSpec.set(key, i);
329
551
  }
330
- if (typeof m.base !== "string" || !/^\/(?:.*\/)?$/.test(m.base)) {
331
- issues.push({ field: "base", message: "\u5FC5\u987B\u662F\u4EE5 / \u5F00\u5934\u4E14 / \u7ED3\u5C3E\u7684\u8DEF\u5F84", got: m.base });
552
+ });
553
+ if (options.schema && options.remotes) {
554
+ pages.forEach((p, i) => {
555
+ const remote = remoteOfRoute(p.route, options.remotes);
556
+ if (!remote) return;
557
+ const entry = options.schema[remote];
558
+ if (!entry || entry.exists === false) return;
559
+ const exposes = new Set((entry.exposes ?? []).map(normSpec));
560
+ if (!exposes.has(effective[i])) {
561
+ violations.push({
562
+ rule: "R3",
563
+ level: "error",
564
+ message: `[R3] \u8DEF\u7531 "${p.route}" \u7684 spec "${effective[i]}" \u4E0D\u5728\u8FDC\u7A0B "${remote}" \u7684 exposes \u6E05\u5355\u4E2D\u3002
565
+ \u6839\u56E0\uFF1Aspec \u62FC\u5199\u4E0E\u8FDC\u7A0B exposes \u952E\u4E0D\u4E00\u81F4\uFF08\u8FD0\u884C\u65F6\u5C06 MFU-006 \u52A0\u8F7D\u5931\u8D25\uFF09\u3002
566
+ \u4FEE\u6CD5\uFF1A\u6838\u5BF9\u8FDC\u7A0B vite.config exposes \u952E\u540D\uFF0C\u6216\u4FEE\u6B63\u672C\u8868 spec\u3002
567
+ \u8FDC\u7A0B\u5B9E\u9645 exposes\uFF08\u524D 10\uFF09\uFF1A${[...exposes].slice(0, 10).join("\u3001") || "\uFF08\u7A7A\uFF09"}`
568
+ });
569
+ }
570
+ });
571
+ }
572
+ pages.forEach((p, i) => {
573
+ pages.forEach((q, j) => {
574
+ if (j <= i) return;
575
+ const kind = shadows(p.route, q.route);
576
+ if (!kind) return;
577
+ violations.push({
578
+ rule: "R4",
579
+ level: "error",
580
+ message: kind === "full" ? `[R4] \u8DEF\u7531\u5B8C\u5168\u91CD\u590D\uFF1A"${p.route}"\uFF08\u7B2C ${i + 1} \u6761\uFF09\u4E0E "${q.route}"\uFF08\u7B2C ${j + 1} \u6761\uFF09\u3002` : `[R4] \u8DEF\u7531 "${q.route}"\uFF08\u7B2C ${j + 1} \u6761\uFF09\u4F1A\u88AB\u66F4\u9760\u524D\u7684 "${p.route}" \u906E\u853D\uFF08\u5339\u914D\u5148\u5230\u5148\u5F97\uFF0C\u540E\u8005\u6210\u6B7B\u8DEF\u7531\uFF09\u3002
581
+ \u4FEE\u6CD5\uFF1A\u8C03\u6574\u6570\u7EC4\u987A\u5E8F\uFF08\u66F4\u5177\u4F53\u7684\u9759\u6001\u8DEF\u7531\u653E\u524D\u9762\uFF09\u6216\u5408\u5E76\u6761\u76EE\u3002`
582
+ });
583
+ });
584
+ });
585
+ const seenName = /* @__PURE__ */ new Map();
586
+ pages.forEach((p, i) => {
587
+ const name = typeof p.name === "string" ? p.name : "";
588
+ if (!name) return;
589
+ if (seenName.has(name)) {
590
+ violations.push({
591
+ rule: "R5",
592
+ level: "warn",
593
+ message: `[R5] name "${name}" \u91CD\u590D\uFF08"${pages[seenName.get(name)].route}" \u4E0E "${p.route}"\uFF09\u2014\u2014vue-router \u547D\u540D\u8DF3\u8F6C push({name}) \u5C06\u547D\u4E2D\u524D\u8005\uFF0C\u8BF7\u6838\u5BF9\u3002`
594
+ });
595
+ } else {
596
+ seenName.set(name, i);
332
597
  }
333
- if (!validAssetRef(m.entry)) {
334
- issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740", got: m.entry });
598
+ });
599
+ return violations;
600
+ }
601
+ var normSpec, hasParam, segs;
602
+ var init_pages = __esm({
603
+ "src/pages.ts"() {
604
+ "use strict";
605
+ normSpec = (s) => s.replace(/^[./]+/, "").replace(/\/+$/, "");
606
+ hasParam = (route) => /(^|\/):[\w]+/.test(route);
607
+ segs = (route) => route.split("/").filter(Boolean);
608
+ }
609
+ });
610
+
611
+ // src/commands.ts
612
+ var commands_exports = {};
613
+ __export(commands_exports, {
614
+ checkPages: () => checkPages,
615
+ explainApp: () => explainApp,
616
+ formatCheckPages: () => formatCheckPages,
617
+ formatExplain: () => formatExplain
618
+ });
619
+ import fs2 from "node:fs";
620
+ import path2 from "node:path";
621
+ function roleOf(app) {
622
+ if (app.host && app.remote) return "dual";
623
+ return app.host ? "host" : "remote";
624
+ }
625
+ function pageMappings(app) {
626
+ const pages = app.host?.pages ?? [];
627
+ const prefixes = Object.entries(app.host?.remotePrefixes ?? {}).sort((a, b) => b[0].length - a[0].length);
628
+ const remoteOf = (route) => {
629
+ for (const [prefix, name] of prefixes) {
630
+ const p = prefix.endsWith("/") ? prefix : `${prefix}/`;
631
+ if (route.startsWith(p) || `${route}/` === p) return name;
335
632
  }
336
- for (const [i, e] of exposes.entries()) {
337
- if (!isPlainObject(e)) {
338
- issues.push({ field: `exposes[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof e });
339
- continue;
340
- }
341
- if (!validExposeName(e.name)) {
342
- issues.push({ field: `exposes[${i}].name`, message: '\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D', got: e.name });
343
- }
344
- if (typeof e.src !== "string" || e.src.length === 0) {
345
- issues.push({ field: `exposes[${i}].src`, message: "dev \u5F62\u6001\u5FC5\u987B\u63D0\u4F9B\u6E90\u7801\u76F8\u5BF9\u8DEF\u5F84", got: e.src });
346
- }
347
- if (!validAssetRef(e.file)) {
348
- issues.push({ field: `exposes[${i}].file`, message: "\u5FC5\u987B\u662F\u53EF\u8BF7\u6C42\u7684\u6A21\u5757\u5730\u5740", got: e.file });
349
- }
633
+ return null;
634
+ };
635
+ const derive = app.host?.deriveSpec ?? ((route) => route.split("/").filter(Boolean).slice(1).filter((s) => !s.startsWith(":")).join("/"));
636
+ const out = [];
637
+ for (const page of pages) {
638
+ const remote = remoteOf(page.route);
639
+ if (!remote) continue;
640
+ const spec = `${remote}/${String(page.spec ?? derive(page.route)).replace(/^[./]+/, "")}`;
641
+ out.push({ route: page.route, remote, spec, title: typeof page.title === "string" ? page.title : void 0 });
642
+ }
643
+ return out;
644
+ }
645
+ async function explainApp(configPath, appPathOrName) {
646
+ const cfg = await loadRepoConfig(configPath);
647
+ const app = cfg.apps.find((a) => a.path === appPathOrName || a.name === appPathOrName);
648
+ if (!app) {
649
+ throw new Error(
650
+ `[fulgurjs:explain] \u5E94\u7528 "${appPathOrName}" \u4E0D\u5728\u914D\u7F6E\u4E2D\uFF08\u53EF\u7528\uFF1A${cfg.apps.map((a) => a.path).join("\u3001")}\uFF09`
651
+ );
652
+ }
653
+ const opts = federationOptionsForApp(cfg, appPathOrName);
654
+ const remotes = Object.entries(opts.remotes ?? {}).map(
655
+ ([key, v]) => ({ key, dev: String(v.dev ?? v.external ?? ""), prod: String(v.prod ?? v.external ?? "") })
656
+ );
657
+ const shared = Object.entries(opts.shared ?? {}).map(
658
+ ([name, v]) => ({ name, singleton: !!v.singleton, requiredVersion: v.requiredVersion ?? false })
659
+ );
660
+ const chain = [];
661
+ chain.push(app.host ? "\u5BBF\u4E3B\u63D0\u4F9B AppContext\uFF08provideAppContext\uFF0C\u542B sessionKey\uFF09\u2192 \u52A0\u8F7D remoteEntry/\u5171\u4EAB\u4F9D\u8D56 \u2192 \u9996\u6B21 loadRemote \u6267\u884C\u8BE5\u8FDC\u7A0B\u53EF\u9009 setup/onSession \u2192 \u53D6\u5F97\u9875\u9762\u6A21\u5757 \u2192 \u5BBF\u4E3B\u5E03\u5C40\u6E32\u67D3" : "\u88AB\u5BBF\u4E3B\u6D88\u8D39\uFF1AremoteEntry/\u5171\u4EAB\u4F9D\u8D56\u52A0\u8F7D \u2192 \u5BBF\u4E3B\u9996\u6B21 loadRemote \u672C\u5E94\u7528\u6A21\u5757\u65F6\u6267\u884C\u53EF\u9009 setup/onSession \u2192 \u9875\u9762\u6A21\u5757\u5728\u5BBF\u4E3B\u5E03\u5C40\u5185\u6E32\u67D3");
662
+ if (app.remote?.setup) {
663
+ chain.push(`setup \u5165\u53E3 ${app.remote.setup}\uFF1A\u9ED8\u8BA4\u5BFC\u51FA\u5E94\u7528\u7EA7\u6267\u884C\u4E00\u6B21\uFF1B${"onSession"} \u6309\u5BBF\u4E3B sessionKey \u53BB\u91CD\uFF08\u7F3A sessionKey \u65F6\u62A5 MFU-013\uFF09`);
664
+ } else {
665
+ chain.push("\u672C\u5E94\u7528\u672A\u914D\u7F6E setup\uFF1A\u65E0\u521D\u59CB\u5316\u751F\u547D\u5468\u671F\uFF0CloadRemote \u76F4\u63A5\u8FD4\u56DE\u6A21\u5757\uFF08\u666E\u901A expose \u8BED\u4E49\uFF09");
666
+ }
667
+ return {
668
+ app: app.name,
669
+ role: roleOf(app),
670
+ path: app.path,
671
+ base: app.base,
672
+ port: app.port,
673
+ remotes,
674
+ exposes: Object.keys(app.remote?.exposes ?? {}),
675
+ setup: app.remote?.setup,
676
+ shared,
677
+ devSharedSelf: {
678
+ value: opts.devSharedSelf === true,
679
+ source: app.devSharedSelf === void 0 ? "inferred" : "explicit"
680
+ },
681
+ pages: pageMappings(app),
682
+ chain
683
+ };
684
+ }
685
+ function formatExplain(r) {
686
+ const roleText = r.role === "dual" ? "\u5BBF\u4E3B+\u8FDC\u7A0B\uFF08\u53CC\u89D2\u8272\uFF09" : r.role === "host" ? "\u5BBF\u4E3B" : "\u8FDC\u7A0B";
687
+ const L = [];
688
+ L.push(`\u5E94\u7528 ${r.app}\uFF08${roleText}\uFF0C\u76EE\u5F55 ${r.path}\uFF0Cdev \u7AEF\u53E3 ${r.port}\uFF0Cbase ${r.base}\uFF09`);
689
+ if (r.remotes.length) {
690
+ L.push("\u6D88\u8D39\u8FDC\u7A0B\uFF1A");
691
+ for (const x of r.remotes) L.push(` ${x.key} \u2192 dev ${x.dev} / prod ${x.prod}`);
692
+ } else {
693
+ L.push("\u6D88\u8D39\u8FDC\u7A0B\uFF1A\uFF08\u65E0\uFF09");
694
+ }
695
+ L.push(`\u516C\u5F00 exposes\uFF08${r.exposes.length}\uFF09\uFF1A${r.exposes.join("\u3001") || "\uFF08\u7A7A\uFF09"}`);
696
+ L.push(`\u5185\u90E8 setup\uFF1A${r.setup ?? "\uFF08\u672A\u914D\u7F6E\u2014\u2014\u65E0\u521D\u59CB\u5316\u751F\u547D\u5468\u671F\uFF09"}`);
697
+ if (r.shared.length) {
698
+ L.push("shared\uFF1A");
699
+ for (const s of r.shared) {
700
+ L.push(` ${s.name}${s.singleton ? " [singleton]" : ""} requiredVersion=${String(s.requiredVersion)}`);
350
701
  }
351
- } else if (isPlainObject(exposes)) {
352
- if (!validAssetRef(m.entry)) {
353
- issues.push({ field: "entry", message: "\u5FC5\u987B\u662F\u5408\u6CD5\u8D44\u6E90\u5730\u5740\uFF08\u76F8\u5BF9\u57FA\u51C6 = \u6240\u5728\u76EE\u5F55\uFF09", got: m.entry });
702
+ }
703
+ L.push(`devSharedSelf\uFF1A${r.devSharedSelf.value}\uFF08\u6765\u6E90\uFF1A${r.devSharedSelf.source === "explicit" ? "\u663E\u5F0F\u914D\u7F6E" : "\u6309\u89D2\u8272\u63A8\u65AD\u2014\u2014\u63D0\u4F9B exposes/setup \u7684\u5E94\u7528\u4E3A true\uFF0C\u7EAF\u5BBF\u4E3B\u4E3A false"}\uFF09`);
704
+ if (r.pages.length) {
705
+ L.push(`\u9875\u9762 spec \u6620\u5C04\uFF08${r.pages.length}\uFF09\uFF1A`);
706
+ for (const p of r.pages) L.push(` ${p.route} \u2192 ${p.spec}${p.title ? `\uFF08${p.title}\uFF09` : ""}`);
707
+ } else {
708
+ L.push("\u9875\u9762 spec \u6620\u5C04\uFF1A\uFF08\u4ED3\u5E93\u914D\u7F6E\u672A\u63D0\u4F9B\u9875\u9762\u8868\u2014\u2014\u5E94\u7528\u4EE3\u7801\u9875\u9762\u8868\u4E3A\u8FD0\u884C\u65F6\u771F\u6E90\uFF09");
709
+ }
710
+ L.push("\u52A0\u8F7D\u94FE\uFF1A");
711
+ for (const c of r.chain) L.push(` \xB7 ${c}`);
712
+ return L.join("\n");
713
+ }
714
+ async function fetchRemoteManifest(cfg, app, opts) {
715
+ const candidates = [app.deployDir || app.base.replace(/^\/|\/$/g, ""), "dist"];
716
+ for (const dir of candidates) {
717
+ const p = path2.join(cfg.root, app.path, dir, "fulgurjs-manifest.json");
718
+ try {
719
+ const parsed = parseManifest(JSON.parse(fs2.readFileSync(p, "utf8")));
720
+ if (parsed.manifest && !parsed.unsupportedVersion) return { manifest: parsed.manifest, from: p };
721
+ } catch {
354
722
  }
355
- for (const [key, v] of Object.entries(exposes)) {
356
- if (!validExposeName(key)) {
357
- issues.push({ field: `exposes["${key}"]`, message: '\u952E\u5FC5\u987B\u662F "./xxx" \u5F62\u6001\u7684 expose \u540D' });
358
- }
359
- if (!isPlainObject(v)) {
360
- issues.push({ field: `exposes["${key}"]`, message: "\u503C\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof v });
361
- continue;
362
- }
363
- if (!validAssetRef(v.file)) {
364
- issues.push({ field: `exposes["${key}"].file`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84\uFF08\u76F8\u5BF9 entry \u76EE\u5F55\uFF09", got: v.file });
365
- }
366
- if (v.css !== void 0) {
367
- if (!Array.isArray(v.css)) {
368
- issues.push({ field: `exposes["${key}"].css`, message: "\u5FC5\u987B\u662F\u5B57\u7B26\u4E32\u6570\u7EC4", got: typeof v.css });
369
- } else {
370
- v.css.forEach((css, i) => {
371
- if (!validAssetRef(css)) {
372
- issues.push({ field: `exposes["${key}"].css[${i}]`, message: "\u5FC5\u987B\u662F\u8D44\u6E90\u8DEF\u5F84", got: css });
373
- }
374
- });
375
- }
723
+ }
724
+ if (opts.site) {
725
+ const base = app.base === "/" ? "" : app.base.replace(/\/$/, "");
726
+ const url = `${opts.site.replace(/\/$/, "")}${base}/fulgurjs-manifest.json`;
727
+ try {
728
+ const res = await fetch(url, { signal: AbortSignal.timeout(5e3) });
729
+ if (res.ok) {
730
+ const parsed = parseManifest(await res.json());
731
+ if (parsed.manifest && !parsed.unsupportedVersion) return { manifest: parsed.manifest, from: url };
376
732
  }
733
+ } catch {
377
734
  }
378
- } else {
379
- issues.push({ field: "exposes", message: "\u5FC5\u987B\u662F\u6570\u7EC4\uFF08dev\uFF09\u6216\u5BF9\u8C61\uFF08prod\uFF09", got: typeof exposes });
380
735
  }
381
- if (issues.length > 0) return { issues };
382
- return { manifest: input, issues };
736
+ return void 0;
383
737
  }
384
- function validateShared(shared) {
385
- if (shared === void 0) return [];
738
+ async function checkPages(configPath, appPathOrName, opts = {}) {
739
+ const cfg = await loadRepoConfig(configPath);
740
+ const app = cfg.apps.find((a) => a.path === appPathOrName || a.name === appPathOrName);
741
+ if (!app) {
742
+ throw new Error(`[fulgurjs:check-pages] \u5E94\u7528 "${appPathOrName}" \u4E0D\u5728\u914D\u7F6E\u4E2D\uFF08\u53EF\u7528\uFF1A${cfg.apps.map((a) => a.path).join("\u3001")}\uFF09`);
743
+ }
386
744
  const issues = [];
387
- if (!Array.isArray(shared)) {
388
- return [{ field: "shared", message: "\u5FC5\u987B\u662F\u6570\u7EC4", got: typeof shared }];
745
+ const pages = app.host?.pages ?? [];
746
+ const prefixes = app.host?.remotePrefixes ?? {};
747
+ if (pages.length === 0) {
748
+ issues.push({
749
+ level: "unverified",
750
+ message: "\u4ED3\u5E93\u914D\u7F6E\u672A\u63D0\u4F9B\u9875\u9762\u8868\uFF08pages \u53EF\u9009\uFF0C\u5E94\u7528\u4EE3\u7801\u9875\u9762\u8868\u4E3A\u8FD0\u884C\u65F6\u771F\u6E90\uFF09\u2014\u2014\u672C\u547D\u4EE4\u65E0\u4ECE\u6838\u5BF9\u3002\n \u4FEE\u6CD5: \u5728 fulgurjs.config.ts \u5BBF\u4E3B host.pages \u63D0\u4F9B\u9875\u9762\u8868\u526F\u672C\uFF0C\u6216\u4F9D\u8D56 dev \u671F remoteSchema \u63A2\u9488\u6821\u9A8C"
751
+ });
752
+ return { app: app.name, checked: 0, issues, failed: false };
389
753
  }
390
- for (const [i, s] of shared.entries()) {
391
- if (!isPlainObject(s)) {
392
- issues.push({ field: `shared[${i}]`, message: "\u5FC5\u987B\u662F\u5BF9\u8C61", got: typeof s });
754
+ const violations = validatePages(pages, { remotes: prefixes });
755
+ for (const v of violations) {
756
+ issues.push({ level: v.level === "error" ? "error" : "warn", message: v.message });
757
+ }
758
+ const remoteNames = [...new Set(Object.values(prefixes))];
759
+ const manifests = /* @__PURE__ */ new Map();
760
+ for (const name of remoteNames) {
761
+ const remoteApp = cfg.apps.find((a) => a.name === name);
762
+ if (!remoteApp) {
763
+ issues.push({ level: "error", message: `\u8DEF\u7531\u524D\u7F00\u6620\u5C04\u5230\u672A\u77E5\u8FDC\u7A0B "${name}"\u2014\u2014\u914D\u7F6E apps \u4E2D\u4E0D\u5B58\u5728\u8BE5\u5BB9\u5668\u540D` });
393
764
  continue;
394
765
  }
395
- if (typeof s.name !== "string" || s.name.length === 0) {
396
- issues.push({ field: `shared[${i}].name`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.name });
766
+ const got = await fetchRemoteManifest(cfg, remoteApp, opts);
767
+ if (!got) {
768
+ issues.push({
769
+ level: "unverified",
770
+ message: `\u8FDC\u7A0B "${name}" \u7684 manifest \u4E0D\u53EF\u5F97\uFF08\u672C\u5730 dist \u4E0E${opts.site ? `\u7AD9\u70B9 ${opts.site}` : "\u9ED8\u8BA4 dist \u8DEF\u5F84"}\u5747\u672A\u547D\u4E2D\uFF09\u2014\u2014\u8BE5\u8FDC\u7A0B\u7684 spec \u5B58\u5728\u6027\u65E0\u6CD5\u6838\u5BF9\u3002
771
+ \u4FEE\u6CD5: \u5148\u6784\u5EFA\u8BE5\u8FDC\u7A0B\uFF08\u4EA7\u7269\u542B fulgurjs-manifest.json\uFF09\uFF0C\u6216\u7528 --site <URL> \u6307\u5411\u5DF2\u90E8\u7F72\u7AD9\u70B9`
772
+ });
773
+ continue;
397
774
  }
398
- if (typeof s.version !== "string" || s.version.length === 0) {
399
- issues.push({ field: `shared[${i}].version`, message: "\u5FC5\u987B\u662F\u975E\u7A7A\u5B57\u7B26\u4E32", got: s.version });
775
+ manifests.set(name, { exposes: new Set([...normalizeExposes(got.manifest).keys()].map(normSpecKey)), from: got.from });
776
+ }
777
+ const derive = app.host?.deriveSpec ?? ((r) => r.split("/").filter(Boolean).slice(1).filter((s) => !s.startsWith(":")).join("/"));
778
+ let checked = 0;
779
+ for (const page of pages) {
780
+ const prefixHit = Object.entries(prefixes).sort((a, b) => b[0].length - a[0].length).find(
781
+ ([p]) => page.route.startsWith(p.endsWith("/") ? p : `${p}/`) || `${page.route}/` === (p.endsWith("/") ? p : `${p}/`)
782
+ );
783
+ if (!prefixHit) {
784
+ issues.push({ level: "error", message: `\u8DEF\u7531 "${page.route}" \u4E0D\u5728\u4EFB\u4F55 remotePrefixes \u524D\u7F00\u4E0B` });
785
+ continue;
786
+ }
787
+ checked++;
788
+ const m = manifests.get(prefixHit[1]);
789
+ if (!m) continue;
790
+ const spec = normSpecKey(String(page.spec ?? derive(page.route)));
791
+ if (!m.exposes.has(spec)) {
792
+ issues.push({
793
+ level: "error",
794
+ message: `\u8DEF\u7531 "${page.route}" \u7684 spec "${spec}" \u4E0D\u5728\u8FDC\u7A0B "${prefixHit[1]}" \u7684 exposes \u6E05\u5355\u4E2D\uFF08\u6765\u6E90 ${m.from}\uFF09\u3002
795
+ \u8FDC\u7A0B\u5B9E\u9645 exposes\uFF08\u524D 10\uFF09\uFF1A${[...m.exposes].slice(0, 10).join("\u3001") || "\uFF08\u7A7A\uFF09"}
796
+ \u4FEE\u6CD5: \u6838\u5BF9\u8FDC\u7A0B federation({ exposes }) \u952E\u540D\u6216\u4FEE\u6B63\u9875\u9762\u8868 spec`
797
+ });
400
798
  }
401
799
  }
402
- return issues;
800
+ return { app: app.name, checked, issues, failed: issues.some((i) => i.level === "error") };
801
+ }
802
+ function formatCheckPages(r) {
803
+ const L = [];
804
+ const errors = r.issues.filter((i) => i.level === "error");
805
+ const unverified = r.issues.filter((i) => i.level === "unverified");
806
+ const warns = r.issues.filter((i) => i.level === "warn");
807
+ L.push(`[fulgurjs:check-pages] \u5E94\u7528 ${r.app}\uFF1A\u6838\u5BF9 ${r.checked} \u6761\u9875\u9762\uFF0Cerror ${errors.length} / warn ${warns.length} / \u65E0\u6CD5\u9A8C\u8BC1 ${unverified.length}`);
808
+ for (const i of [...errors, ...warns, ...unverified]) L.push(`
809
+ [${i.level.toUpperCase()}] ${i.message}`);
810
+ if (r.issues.length === 0) L.push("\u5168\u90E8\u9875\u9762 spec \u4E0E\u8FDC\u7A0B exposes \u4E00\u81F4 \u2713");
811
+ return L.join("\n");
403
812
  }
813
+ var normSpecKey;
814
+ var init_commands = __esm({
815
+ "src/commands.ts"() {
816
+ "use strict";
817
+ init_config();
818
+ init_config();
819
+ init_pages();
820
+ init_manifest();
821
+ normSpecKey = (s) => s.replace(/^[./]+/, "").replace(/\/+$/, "");
822
+ }
823
+ });
824
+
825
+ // src/cli.ts
826
+ import { resolve } from "node:path";
404
827
 
405
828
  // src/doctor.ts
829
+ init_manifest();
830
+ import http from "node:http";
831
+ import https from "node:https";
406
832
  function fetchHeadOrGet(url, method = "GET") {
407
833
  return new Promise((resolve2, reject) => {
408
834
  const mod = url.startsWith("https:") ? https : http;
@@ -713,12 +1139,18 @@ var HELP = `fulgurjs \u2014 Vite Module Federation CLI (@fulgurjs/federation)
713
1139
  fulgurjs init [--template <path>] [--force] \u751F\u6210\u5E26\u6CE8\u91CA\u7684 fulgurjs.config.ts \u8D77\u6B65\u6A21\u677F
714
1140
  fulgurjs init --config <path> \u6821\u9A8C\u914D\u7F6E\uFF1B\u8F93\u51FA\u5404\u5E94\u7528 federation() \u7C98\u8D34\u5757\u3001
715
1141
  NGINX no-cache \u7AD9\u70B9\u6A21\u677F\u4E0E\u63A5\u5165\u6838\u5BF9\u6E05\u5355
1142
+ fulgurjs explain --config <path> --app <name> [--json]
1143
+ \u89E3\u91CA\u67D0\u5E94\u7528\u7684\u6709\u6548\u8054\u90A6\u5F62\u6001\u4E0E\u52A0\u8F7D\u94FE\uFF08\u7EAF\u672C\u5730\uFF0C\u65E0\u7F51\u7EDC\uFF09
1144
+ fulgurjs check-pages --config <path> --app <name> [--site <URL>] [--json]
1145
+ \u6838\u5BF9\u5BBF\u4E3B\u9875\u9762\u8868\u4E0E\u8FDC\u7A0B exposes\uFF08\u672C\u5730 dist \u4F18\u5148\uFF1B\u786E\u5B9A\u6027\u9519\u8BEF\u975E\u96F6\u9000\u51FA\uFF09
716
1146
  fulgurjs doctor --base <URL> --apps <a,b,c> [--dev] [--json] [--chunk-sample N]
717
1147
  fulgurjs --help
718
1148
 
719
1149
  \u793A\u4F8B\uFF1A
720
1150
  fulgurjs init # \u5F53\u524D\u76EE\u5F55\u5199 fulgurjs.config.ts\uFF08\u5DF2\u5B58\u5728\u5219\u62D2\u7EDD\uFF0C--force \u8986\u76D6\uFF09
721
1151
  fulgurjs init --config fulgurjs.config.ts # \u6821\u9A8C + \u8F93\u51FA\u6837\u677F
1152
+ fulgurjs explain --config fulgurjs.config.ts --app apps/remote-a
1153
+ fulgurjs check-pages --config fulgurjs.config.ts --app apps/host --site http://your-site
722
1154
  fulgurjs doctor --base http://your-site --apps app-a,app-b
723
1155
  fulgurjs doctor --base http://localhost:5173 --apps app-a --dev
724
1156
  `;
@@ -756,10 +1188,35 @@ async function main() {
756
1188
  \u540E\u7EED\u6B65\u9AA4\uFF1A
757
1189
  1. \u7F16\u8F91 fulgurjs.config.ts\uFF1A\u586B\u5165\u4F60\u7684\u5E94\u7528\u76EE\u5F55/\u5BB9\u5668\u540D/\u7AEF\u53E3/base/\u9875\u9762\u8DEF\u7531\u8868/exposes
758
1190
  2. npx fulgurjs init --config fulgurjs.config.ts # \u6821\u9A8C\u5E76\u8F93\u51FA\u53EF\u7C98\u8D34\u6837\u677F\u4E0E\u6838\u5BF9\u6E05\u5355
759
- 3. \u6309\u6E05\u5355\u628A federation() \u5757\u63A5\u5165\u5404\u5E94\u7528 vite.config.ts\uFF0C\u5B89\u88C5\u4F9D\u8D56
760
- 4. \u90E8\u7F72\u540E\uFF1Anpx fulgurjs doctor --base <URL> --apps <\u5E94\u7528...>`);
1191
+ 3. \u6309\u6E05\u5355\u628A federation(federationOptionsForApp(...)) \u63A5\u5165\u5404\u5E94\u7528 vite.config.ts\uFF08\u5355\u914D\u7F6E\u9A71\u52A8\uFF0C\u89C1 README\uFF09\uFF0C\u5B89\u88C5\u4F9D\u8D56
1192
+ 4. npx fulgurjs explain --config fulgurjs.config.ts --app <\u5E94\u7528> # \u6838\u5BF9\u6709\u6548\u5F62\u6001\u4E0E\u52A0\u8F7D\u94FE
1193
+ 5. \u90E8\u7F72\u540E\uFF1Anpx fulgurjs doctor --base <URL> --apps <\u5E94\u7528...>`);
761
1194
  return 0;
762
1195
  }
1196
+ if (cmd === "explain" || cmd === "check-pages") {
1197
+ const configPath = argOf("--config");
1198
+ const app = argOf("--app");
1199
+ if (!configPath || !app) {
1200
+ console.error(`[fulgurjs:${cmd}] \u7F3A\u5C11\u53C2\u6570\uFF1A--config <fulgurjs.config.ts \u8DEF\u5F84> \u4E0E --app <\u5E94\u7528\u76EE\u5F55\u540D\u6216\u5BB9\u5668\u540D>`);
1201
+ return 2;
1202
+ }
1203
+ try {
1204
+ if (cmd === "explain") {
1205
+ const { explainApp: explainApp2, formatExplain: formatExplain2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
1206
+ const r2 = await explainApp2(resolve(configPath), app);
1207
+ console.log(has("--json") ? JSON.stringify(r2, null, 2) : formatExplain2(r2));
1208
+ return 0;
1209
+ }
1210
+ const { checkPages: checkPages2, formatCheckPages: formatCheckPages2 } = await Promise.resolve().then(() => (init_commands(), commands_exports));
1211
+ const site = argOf("--site");
1212
+ const r = await checkPages2(resolve(configPath), app, site ? { site } : {});
1213
+ console.log(has("--json") ? JSON.stringify(r, null, 2) : formatCheckPages2(r));
1214
+ return r.failed ? 1 : 0;
1215
+ } catch (e) {
1216
+ console.error(String(e.message ?? e));
1217
+ return 2;
1218
+ }
1219
+ }
763
1220
  if (cmd === "doctor") {
764
1221
  const base = argOf("--base");
765
1222
  const appsRaw = argOf("--apps") ?? "main";