@morlay/dsh-client-modules 0.0.0-stage → 0.2.6
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/LICENSE +21 -0
- package/README.md +23 -2
- package/app-boot/base.css +187 -0
- package/app-boot/boot-page.ts +110 -0
- package/app-boot/index.html +15 -0
- package/app-boot/index.ts +101 -0
- package/app-boot/node-stub.cjs +19 -0
- package/app-boot/node-stub.ts +23 -0
- package/dist/client.cjs +66 -0
- package/dist/client.d.cts +9 -0
- package/dist/client.d.mts +9 -0
- package/dist/client.mjs +57 -0
- package/dist/index.cjs +1445 -0
- package/dist/index.d.mts +13 -0
- package/dist/index.mjs +1442 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +65 -3
- package/src/__tests__/bundle.spec.ts +235 -0
- package/src/__tests__/resolve.spec.ts +127 -0
- package/src/bundle.ts +708 -0
- package/src/client/__tests__/modules-service.spec.ts +43 -0
- package/src/client/__tests__/terminal-recovery.spec.ts +65 -0
- package/src/client/index.ts +101 -0
- package/src/config.ts +70 -0
- package/src/dirs.ts +97 -0
- package/src/index.ts +695 -0
- package/src/resolve.ts +418 -0
- package/src/serve-vite.ts +211 -0
- package/src/trace.ts +25 -0
- package/src/vite-plugin.ts +240 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1445 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let node_fs_promises = require("node:fs/promises");
|
|
3
|
+
let node_module = require("node:module");
|
|
4
|
+
let node_path = require("node:path");
|
|
5
|
+
let node_url = require("node:url");
|
|
6
|
+
let _morlay_dsh_cordis_assets_assets = require("@morlay/dsh-cordis-assets/assets");
|
|
7
|
+
let node_crypto = require("node:crypto");
|
|
8
|
+
let _morlay_dsh_cordis_assets_styles = require("@morlay/dsh-cordis-assets/styles");
|
|
9
|
+
let rolldown = require("rolldown");
|
|
10
|
+
let vite = require("vite");
|
|
11
|
+
//#region src/trace.ts
|
|
12
|
+
/**
|
|
13
|
+
* 诊断细节(逐包预编译、逐请求那一类)的具名 logger。
|
|
14
|
+
*
|
|
15
|
+
* 名字交给 logger(`ctx.logger(name)`);**值走参数**、不拼进文本——cordis 的 logger 没有"每次调用带
|
|
16
|
+
* meta"这回事(`meta` 是 logger 实例级的),所以结构化的东西作为 `%o` 的参数传(exporter 拿到的
|
|
17
|
+
* `args` 里是结构与值,而不是一段 `key=value` 文本)。键名若出现,用 `.` 分层、不带空格。
|
|
18
|
+
*
|
|
19
|
+
* **是否调用**由本开关决定:运行时的日志门槛不由我们定(loader 注册的 exporter 允许 DEBUG),而这类细节
|
|
20
|
+
* 一次启动两千多行、每请求一行,默认不调,`DSH_TRACE=1` 时再调。出问题的几行走 `warn`。
|
|
21
|
+
* @param ctx - 这条链的上下文。
|
|
22
|
+
* @param name - 这个 logger 的名字(如 `federation` / `vite`)。
|
|
23
|
+
* @returns `(format, ...args)` 形态的诊断出口;开关没开时什么都不做。
|
|
24
|
+
*/
|
|
25
|
+
function traceOf(ctx, name) {
|
|
26
|
+
if (process.env["DSH_TRACE"] !== "1") return () => {};
|
|
27
|
+
const logger = ctx.logger(name);
|
|
28
|
+
return (format, ...args) => {
|
|
29
|
+
logger.debug(format, ...args);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/config.ts
|
|
34
|
+
/**
|
|
35
|
+
* dev 形态的分流配置:profile 的 `dsh.dev.exclude` 点名的包**不算开发模块**。
|
|
36
|
+
*
|
|
37
|
+
* 那些包(如 `@deepseek-ai/*`)在 dev 期间不打算改,于是不值得为它们付"逐文件请求"的代价——
|
|
38
|
+
* 走按包聚合(一个包一份产物、共享 chunk),换掉模块级 HMR;没被点名的包是正在改的,保持逐文件走 vite。
|
|
39
|
+
*/
|
|
40
|
+
/** 一条 glob(只支持 `*`,够用)→ 正则。 */
|
|
41
|
+
function globToRegExp(pattern) {
|
|
42
|
+
const escaped = pattern.replace(/[/\\^$+?.()|[\]{}]/gu, "\\$&").replaceAll("*", ".*");
|
|
43
|
+
return new RegExp(`^${escaped}$`);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 读 profile 的 `dsh.dev.exclude`。
|
|
47
|
+
*
|
|
48
|
+
* profile 根按这个顺序找:启动器给的 `DSH_DESKTOP_DEV_PROFILE_DIR`(桌面形态);`DSH_HOME` 的父目录
|
|
49
|
+
* (它总是 `<profile>/.dsh-store`,web 形态也有);当前目录。
|
|
50
|
+
*
|
|
51
|
+
* @returns 匹配"非开发模块"的正则表;**没配就是全部**(见函数末尾的说明)。
|
|
52
|
+
*/
|
|
53
|
+
/**
|
|
54
|
+
* 桌面形态(含 bundle):**全部按非开发模块**处理。
|
|
55
|
+
*
|
|
56
|
+
* 它的页面跑在自定义协议下(`dsh-…://app/`),请求经壳的字节管道进来——每个逐文件请求都要走一趟这条路,
|
|
57
|
+
* 几百个源码模块叠起来就是"刷新一次卡几十秒"。桌面形态本来也不需要"改哪个包只看哪个包"的细粒度取舍:
|
|
58
|
+
* 全部按包预编译,改包时那一份重建 + 通知页面刷新(见壳的 ws 端点)。
|
|
59
|
+
*/
|
|
60
|
+
/**
|
|
61
|
+
* 是不是**开发形态**(`dshdev web` / `dshdev desktop`;打包产物不是)。
|
|
62
|
+
*
|
|
63
|
+
* `DSH_DEV_FORM` 由 `dshdev` 注入给宿主(壳起宿主时会过滤掉 `DSH_DESKTOP_*`,那些是 Electron 侧的元数据;
|
|
64
|
+
* web 形态没有壳,所以这条由 CLI 直接给)——值只说哪种形态,**有没有值**才是判据。这一判据**一个 home**:
|
|
65
|
+
* 预编译策略(本文件)与"这条链的根在哪"(`src/index.ts` 的 `repoRootOf`)都用它。
|
|
66
|
+
*
|
|
67
|
+
* @returns dev 形态为 true;bundle(打包产物)为 false。
|
|
68
|
+
*/
|
|
69
|
+
function isDevForm() {
|
|
70
|
+
return process.env["DSH_DEV_FORM"] !== void 0;
|
|
71
|
+
}
|
|
72
|
+
async function devExcluded(profileDir) {
|
|
73
|
+
if (!isDevForm()) return [/.*/u];
|
|
74
|
+
try {
|
|
75
|
+
const patterns = (JSON.parse(await (0, node_fs_promises.readFile)((0, node_path.join)(profileDir, "package.json"), "utf8")).dsh?.dev?.exclude ?? []).map(globToRegExp);
|
|
76
|
+
if (patterns.length > 0) return patterns;
|
|
77
|
+
} catch {}
|
|
78
|
+
return [/.*/u];
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 该包是否走**聚合**(即"非开发模块")。
|
|
82
|
+
*
|
|
83
|
+
* @param name - 包名。
|
|
84
|
+
* @param patterns - {@link devExcluded} 的结果。
|
|
85
|
+
* @returns 命中任一 glob 即为真。
|
|
86
|
+
*/
|
|
87
|
+
function isAggregated(name, patterns) {
|
|
88
|
+
return patterns.some((pattern) => pattern.test(name));
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/resolve.ts
|
|
92
|
+
/** 路由前缀(沿用上游的 `/plugins`,页面上不改 URL 形态)。 */
|
|
93
|
+
const PLUGIN_ROUTE = "/plugins";
|
|
94
|
+
/** `@deepseek-ai/dsh-x` → `@deepseek-ai+dsh-x`:包名里的 `/` 在 URL 段里要转义。 */
|
|
95
|
+
/**
|
|
96
|
+
* **出口名 ↔ 产物文件名**的约定:出口产物的文件名是 `<出口名>.js`。
|
|
97
|
+
*
|
|
98
|
+
* 这条规则**只有一处**(这里):`urlOf` 生成地址、产物路由解析地址,两边都调它——各写一遍"补 `.js` / 剥
|
|
99
|
+
* `.js`"就会漂(`remote` 与 `remote.js` 两种地址,明明产出过却查不到)。
|
|
100
|
+
*/
|
|
101
|
+
function entryFileNameOf(exportName) {
|
|
102
|
+
return `${exportName}.js`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 地址里的文件名 → 出口名({@link entryFileNameOf} 的反向)。
|
|
106
|
+
*
|
|
107
|
+
* @param file - 地址里最后一段(`client.js`、`remote.js`、`chunks/…`)。
|
|
108
|
+
* @returns 出口名;没有扩展名的按原样(`index` 是包级引用的缺省出口)。
|
|
109
|
+
*/
|
|
110
|
+
function exportNameOfFileName(file) {
|
|
111
|
+
return file.replace(/\.js$/u, "") || "index";
|
|
112
|
+
}
|
|
113
|
+
function encodePackageName(name) {
|
|
114
|
+
return name.replace("/", "+");
|
|
115
|
+
}
|
|
116
|
+
function decodePackageName(segment) {
|
|
117
|
+
return segment.replace("+", "/");
|
|
118
|
+
}
|
|
119
|
+
function parsePluginAddress(url) {
|
|
120
|
+
const path = url.split("?")[0] ?? "";
|
|
121
|
+
if (!path.startsWith(`/plugins/`)) return void 0;
|
|
122
|
+
const segments = path.slice(9).split("/").filter((segment) => segment !== "");
|
|
123
|
+
if (segments[0]?.startsWith("??") === true || segments[0]?.startsWith(".") === true) return void 0;
|
|
124
|
+
const [pkgSegment, version, ...exportSegments] = segments;
|
|
125
|
+
if (pkgSegment === void 0 || version === void 0) return void 0;
|
|
126
|
+
const exportSegment = exportSegments.join("/");
|
|
127
|
+
return {
|
|
128
|
+
name: decodePackageName(pkgSegment),
|
|
129
|
+
version,
|
|
130
|
+
exportName: exportSegment === "" || exportSegment === "_default" ? "." : `./${exportSegment}`
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* 条件导出里挑**运行期**入口:`import` > `default` > `require` > `types`。
|
|
135
|
+
*
|
|
136
|
+
* 条件是**可嵌套**的(`zustand` 的 `./vanilla` 出口就是
|
|
137
|
+
* `{ import: { types, default }, default: { types, default } }`),所以命中条件后要继续往里挑——
|
|
138
|
+
* 只看顶层字符串会跳过 `import` 落到 `default`,拿到 CJS 那支(`vanilla.js` 而不是 `esm/vanilla.mjs`),
|
|
139
|
+
* 产物里就只剩 `export default require_…`,消费方要的具名导出一个都没有。
|
|
140
|
+
*/
|
|
141
|
+
function pickCondition(entry) {
|
|
142
|
+
if (typeof entry === "string") return entry;
|
|
143
|
+
if (Array.isArray(entry)) {
|
|
144
|
+
for (const item of entry) {
|
|
145
|
+
const picked = pickCondition(item);
|
|
146
|
+
if (picked !== void 0) return picked;
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (entry === null || typeof entry !== "object") return void 0;
|
|
151
|
+
const record = entry;
|
|
152
|
+
for (const key of [
|
|
153
|
+
"import",
|
|
154
|
+
"default",
|
|
155
|
+
"require",
|
|
156
|
+
"types"
|
|
157
|
+
]) {
|
|
158
|
+
if (!(key in record)) continue;
|
|
159
|
+
const picked = pickCondition(record[key]);
|
|
160
|
+
if (picked !== void 0) return picked;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* 工作区包索引:包名 → 包根。
|
|
165
|
+
*
|
|
166
|
+
* `pnpm` 的布局只保证"**能解析的**才在"——vendor 里的包、以及当前包没声明为依赖的工作区包,从 importer
|
|
167
|
+
* 出发都解析不到。但它们是**仓库内的包**(有自己的 `exports`、自己的 `./client` 出口),不该被当成三方。
|
|
168
|
+
* 所以扫一遍仓库内的清单,懒建一次索引。
|
|
169
|
+
*/
|
|
170
|
+
let workspaceIndex;
|
|
171
|
+
/** 从某个文件向上找仓库根(`pnpm-workspace.yaml` 那一层)。 */
|
|
172
|
+
async function workspaceRootOf(from) {
|
|
173
|
+
let directory = (0, node_path.dirname)(from);
|
|
174
|
+
for (;;) {
|
|
175
|
+
if (await (0, node_fs_promises.readFile)((0, node_path.join)(directory, "pnpm-workspace.yaml"), "utf8").then(() => true, () => false)) return directory;
|
|
176
|
+
const parent = (0, node_path.dirname)(directory);
|
|
177
|
+
if (parent === directory) return void 0;
|
|
178
|
+
directory = parent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function workspaceIndexOf(from) {
|
|
182
|
+
workspaceIndex ??= (async () => {
|
|
183
|
+
const index = /* @__PURE__ */ new Map();
|
|
184
|
+
const root = await workspaceRootOf(from);
|
|
185
|
+
if (root === void 0) return index;
|
|
186
|
+
const { glob } = await import("node:fs/promises");
|
|
187
|
+
for (const pattern of [
|
|
188
|
+
"packages/*/*/package.json",
|
|
189
|
+
"vendor/*/packages/*/*/package.json",
|
|
190
|
+
"vendor/*/vendor/*/package.json"
|
|
191
|
+
]) for await (const file of glob(pattern, { cwd: root })) {
|
|
192
|
+
const path = (0, node_path.join)(root, file);
|
|
193
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8").catch(() => "{}"));
|
|
194
|
+
if (manifest.name !== void 0) index.set(manifest.name, (0, node_path.dirname)(path));
|
|
195
|
+
}
|
|
196
|
+
return index;
|
|
197
|
+
})();
|
|
198
|
+
return await workspaceIndex;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 包名 → 包根:走 node 的解析(pnpm 已经把依赖摆好了),**解析入口**再向上找最近的清单。
|
|
202
|
+
*
|
|
203
|
+
* 不能用 `resolve('<name>/package.json')`:现代包(`clsx` 这类)的 `exports` 通常不暴露
|
|
204
|
+
* `./package.json`,那样明明装着也解析不出来。
|
|
205
|
+
*/
|
|
206
|
+
async function packageRootOf(from, name) {
|
|
207
|
+
const bare = name.split("/").slice(0, name.startsWith("@") ? 2 : 1).join("/");
|
|
208
|
+
const require$2 = from.startsWith("\0") ? void 0 : (0, node_module.createRequire)(from);
|
|
209
|
+
try {
|
|
210
|
+
if (require$2 === void 0) throw new Error("virtual id: 不走 node 解析");
|
|
211
|
+
const owned = await owningPackageOf(require$2.resolve(bare));
|
|
212
|
+
if (owned !== void 0) return owned;
|
|
213
|
+
} catch {}
|
|
214
|
+
for (const directory of require$2?.resolve.paths(bare) ?? []) {
|
|
215
|
+
const manifest = (0, node_path.join)(directory, bare, "package.json");
|
|
216
|
+
if (await (0, node_fs_promises.readFile)(manifest, "utf8").then(() => true, () => false)) {
|
|
217
|
+
const owned = await owningPackageOf(manifest);
|
|
218
|
+
if (owned !== void 0) return owned;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const root = (await workspaceIndexOf(from)).get(name);
|
|
222
|
+
return root === void 0 ? void 0 : await owningPackageOf((0, node_path.join)(root, "package.json"));
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* 按包清单的 `exports` 解析子路径(`./client` → `./src/client/index.ts`)。
|
|
226
|
+
*
|
|
227
|
+
* 不能自己拼 `src/<subpath>`:出口名与文件名**不同构**(`./client` 指向 `src/client/index.ts`、
|
|
228
|
+
* `./remote` 指向 `lib/typert.remote-client.ts` 这类都由清单说了算)。
|
|
229
|
+
*/
|
|
230
|
+
async function exportTargetOf(root, subpath) {
|
|
231
|
+
try {
|
|
232
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)((0, node_path.join)(root, "package.json"), "utf8"));
|
|
233
|
+
const key = subpath === void 0 ? "." : `./${subpath}`;
|
|
234
|
+
const table = manifest.exports;
|
|
235
|
+
const entry = (table !== void 0 && Object.keys(table).some((name) => name === "." || name.startsWith("./")) ? table?.[key] : key === "." ? table : void 0) ?? (key === "." ? manifest.module ?? manifest.main : void 0);
|
|
236
|
+
const target = typeof entry === "string" ? entry : entry === null || typeof entry !== "object" ? void 0 : pickCondition(entry);
|
|
237
|
+
if (typeof target !== "string") return void 0;
|
|
238
|
+
return resolveInPackage({
|
|
239
|
+
name: "",
|
|
240
|
+
version: "",
|
|
241
|
+
root
|
|
242
|
+
}, target.replace(/^\.\//u, ""));
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** 读包清单。 */
|
|
248
|
+
async function manifestOf(root) {
|
|
249
|
+
try {
|
|
250
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)((0, node_path.join)(root, "package.json"), "utf8"));
|
|
251
|
+
if (manifest.name === void 0) return void 0;
|
|
252
|
+
return {
|
|
253
|
+
...manifest,
|
|
254
|
+
name: manifest.name,
|
|
255
|
+
version: manifest.version ?? "0"
|
|
256
|
+
};
|
|
257
|
+
} catch {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** 向上找最近的包(软链已由 node 解析解开):返回清单与包根。 */
|
|
262
|
+
async function owningPackageOf(file) {
|
|
263
|
+
let directory = (0, node_path.dirname)(file);
|
|
264
|
+
while (directory !== (0, node_path.dirname)(directory)) {
|
|
265
|
+
const manifest = await manifestOf(directory);
|
|
266
|
+
if (manifest !== void 0) return {
|
|
267
|
+
...manifest,
|
|
268
|
+
root: directory
|
|
269
|
+
};
|
|
270
|
+
directory = (0, node_path.dirname)(directory);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/** 包根 + 包内路径 → 绝对路径;越出包根一律拒绝(寻址来自 URL,不能穿透到仓库别处)。 */
|
|
274
|
+
function resolveInPackage(pkg, filepath) {
|
|
275
|
+
const target = (0, node_path.resolve)(pkg.root, filepath);
|
|
276
|
+
const inside = (0, node_path.relative)(pkg.root, target);
|
|
277
|
+
return inside === "" || inside.startsWith("..") || inside.startsWith(node_path.sep) ? void 0 : target;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* 包内**物理路径**回退:清单的 `exports` 没覆盖这个子路径时,按约定位置找文件。
|
|
281
|
+
*
|
|
282
|
+
* 老包很常见:`picomatch` 没有 `exports`、只有 `main`,而 `picomatch/posix` 就是 `posix.js`;
|
|
283
|
+
* 只声明了部分子路径的包同理。寻址按 URL 来(`<pkg>@<version>/<子路径>.js`),所以这里按子路径找文件。
|
|
284
|
+
*/
|
|
285
|
+
async function fileInPackage(root, subpath) {
|
|
286
|
+
const pkg = {
|
|
287
|
+
name: "",
|
|
288
|
+
version: "",
|
|
289
|
+
root
|
|
290
|
+
};
|
|
291
|
+
for (const candidate of [
|
|
292
|
+
subpath,
|
|
293
|
+
`${subpath}.js`,
|
|
294
|
+
`${subpath}.mjs`,
|
|
295
|
+
(0, node_path.join)(subpath, "index.js"),
|
|
296
|
+
(0, node_path.join)(subpath, "index.mjs"),
|
|
297
|
+
(0, node_path.join)(subpath, "index.ts")
|
|
298
|
+
]) {
|
|
299
|
+
const file = resolveInPackage(pkg, candidate);
|
|
300
|
+
if (file !== void 0 && await (0, node_fs_promises.readFile)(file, "utf8").then(() => true, () => false)) return file;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* 说明符里的包名:`@scope/name` 取两段、`name` 取一段。作用域包**没有子路径**时,
|
|
305
|
+
* `indexOf("/", slash + 1)` 会返回 -1——直接 `slice` 会削掉最后一个字符,这里显式判掉。
|
|
306
|
+
*/
|
|
307
|
+
function packageNameOf(specifier) {
|
|
308
|
+
if (!specifier.startsWith("@")) return specifier.split("/")[0] ?? specifier;
|
|
309
|
+
const slash = specifier.indexOf("/");
|
|
310
|
+
if (slash < 0) return specifier;
|
|
311
|
+
const second = specifier.indexOf("/", slash + 1);
|
|
312
|
+
return second < 0 ? specifier : specifier.slice(0, second);
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* 包根 → 它**浏览器面**的文件。
|
|
316
|
+
*
|
|
317
|
+
* @param root - 包根目录。
|
|
318
|
+
* @param subpath - 出口名(不含包名,如 `client`、`brand-font.css`);省略表示包的根出口。
|
|
319
|
+
* @returns 该出口对应的文件;清单没这个出口、且约定路径也不存在时返回 undefined。
|
|
320
|
+
*/
|
|
321
|
+
async function clientHalfOf(root, subpath) {
|
|
322
|
+
const exists = async (file) => await (0, node_fs_promises.readFile)(file, "utf8").then(() => true, () => false);
|
|
323
|
+
if (subpath !== void 0) {
|
|
324
|
+
const byExports = await exportTargetOf(root, subpath);
|
|
325
|
+
if (byExports !== void 0) return byExports;
|
|
326
|
+
const pkg = {
|
|
327
|
+
name: "",
|
|
328
|
+
version: "",
|
|
329
|
+
root
|
|
330
|
+
};
|
|
331
|
+
for (const candidate of [
|
|
332
|
+
(0, node_path.join)("src", subpath, "index.ts"),
|
|
333
|
+
(0, node_path.join)(subpath, "index.ts"),
|
|
334
|
+
subpath
|
|
335
|
+
]) {
|
|
336
|
+
const file = resolveInPackage(pkg, candidate);
|
|
337
|
+
if (file !== void 0 && await exists(file)) return file;
|
|
338
|
+
}
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const byClient = await exportTargetOf(root, "client");
|
|
342
|
+
if (byClient !== void 0) return byClient;
|
|
343
|
+
const conventional = (0, node_path.join)(root, "src", "client", "index.ts");
|
|
344
|
+
return await exists(conventional) ? conventional : void 0;
|
|
345
|
+
}
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/bundle.ts
|
|
348
|
+
/**
|
|
349
|
+
* 客户端资源的**联邦式**产出:一个包一份产物,三方的与我们的**没有本质区别**。
|
|
350
|
+
*
|
|
351
|
+
* vite 8 底下就是 rolldown,转换规则、插件钩子、CSS 与资产的处理都在同一套配置里——所以这里复用
|
|
352
|
+
* dev 那份配置(`cssInlinePlugins` / `clientAssetPlugin` / alias / define),只把它跑在 build 模式上:
|
|
353
|
+
* **一次构建、产物按出口分开**(多 entry + 共享 chunk)、落盘缓存、按输入指纹失效。
|
|
354
|
+
*
|
|
355
|
+
* 差别只在"入口从哪儿来":我们的包按 `exports` 找它的 client 半,三方按 node 的解析找它的入口。
|
|
356
|
+
* 两者产出的地址同构(`@client-modules/<pkg>@<version>/<file>`),所以运行时按 URL 取即可。
|
|
357
|
+
*/
|
|
358
|
+
/** 裸包名 → `^名(/|$)`:既覆盖子路径引用,也不会把 `node:fs`、`./x` 误判成包。 */
|
|
359
|
+
function barePackageRegExp(name) {
|
|
360
|
+
return new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")}(?:/|$)`, "u");
|
|
361
|
+
}
|
|
362
|
+
/** 统一前缀:三方与我们的包共用同一个命名空间。 */
|
|
363
|
+
const CLIENT_MODULES_PREFIX = "/@client-modules";
|
|
364
|
+
/** 统一缓存目录(与 vite 的 `.vite` 并列,跨进程共用)。 */
|
|
365
|
+
const CLIENT_MODULES_CACHE = (0, node_path.join)("node_modules", ".dsh-client-modules");
|
|
366
|
+
/**
|
|
367
|
+
* 客户端资源链的**解析约定**(dev 的 vite server 与联邦构建各持一份,规则相同)。
|
|
368
|
+
*
|
|
369
|
+
* - `@/…` = 仓库根的文件(源码里引用仓库内文件都用这个前缀,不写死绝对路径);
|
|
370
|
+
* - `node:*` = 求值安全的空替身文件。**整串匹配**:`find: /^node:/` 是前缀替换,会把 `node:fs/promises`
|
|
371
|
+
* 拼成 `<替身路径>fs/promises`(构建期报 UNLOADABLE_DEPENDENCY)。
|
|
372
|
+
*
|
|
373
|
+
* 替身的**形态**两条链不同(dev 是 ESM、构建要 CJS,见 `app-boot/node-stub.*` 里各自的说明),
|
|
374
|
+
* 所以由调用方给路径。
|
|
375
|
+
*/
|
|
376
|
+
function clientModulesAlias(options) {
|
|
377
|
+
return [{
|
|
378
|
+
find: /^@\//u,
|
|
379
|
+
replacement: `${options.repoRoot}/`
|
|
380
|
+
}, {
|
|
381
|
+
find: _morlay_dsh_cordis_assets_assets.NODE_BUILTIN,
|
|
382
|
+
replacement: options.nodeStub
|
|
383
|
+
}];
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 地址里的 `?v=`(与 vite 同一个写法):构建器**规则指纹**(`BUNDLER_RULES` 的哈希,形如 vite 的 `?v=`)。
|
|
387
|
+
*
|
|
388
|
+
* 它只随"这条链的规则"变化——一次会话内所有地址一致(同一个包只有一个 URL,多一个 URL 就多一份模块
|
|
389
|
+
* 实例)。内容变了**不换 URL**,靠 `cache-control: no-cache` + `ETag` 协商(vite dev 对它自己的模块
|
|
390
|
+
* 也是这套)。
|
|
391
|
+
*/
|
|
392
|
+
const BUNDLER_REV = (0, node_crypto.createHash)("sha1").update(`client-modules:rules:22`).digest("hex").slice(0, 8);
|
|
393
|
+
/** 包 → 它在浏览器里的地址(联邦式)。 */
|
|
394
|
+
function depsUrlOf(specifier, version, file = "index.js") {
|
|
395
|
+
const name = packageNameOf(specifier) || specifier;
|
|
396
|
+
return `${CLIENT_MODULES_PREFIX}/${encodePackageName(name)}@${version}/${file}`;
|
|
397
|
+
}
|
|
398
|
+
/** 缓存目录名(也是产物目录的键):`<pkg>@<version>`,scope 的 `/` 转义成 `+`。 */
|
|
399
|
+
function cacheKeyOf(name, version) {
|
|
400
|
+
return `${encodePackageName(name)}@${version}`;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 目录/文件的内容指纹(名字 + 内容)。
|
|
404
|
+
*
|
|
405
|
+
* 先按**元信息**(名字 + 大小 + mtime)攒一个快速戳:戳没变就直接复用上次的指纹——否则每次请求都要把
|
|
406
|
+
* 每个包的源码**读一遍**(几百个包 × 每个包几十个文件),首屏全耗在算指纹上。
|
|
407
|
+
*/
|
|
408
|
+
const fingerprints = /* @__PURE__ */ new Map();
|
|
409
|
+
async function fingerprint(paths) {
|
|
410
|
+
const key = paths.join("|");
|
|
411
|
+
const stampHash = (0, node_crypto.createHash)("sha1");
|
|
412
|
+
const collect = async (path) => {
|
|
413
|
+
const info = await (0, node_fs_promises.stat)(path).catch(() => void 0);
|
|
414
|
+
if (info === void 0) return;
|
|
415
|
+
if (!info.isDirectory()) {
|
|
416
|
+
stampHash.update(`${(0, node_path.relative)(paths[0] ?? path, path).split(node_path.sep).join("/")}:${String(info.size)}:${String(info.mtimeMs)}`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true }).catch(() => []);
|
|
420
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) await collect((0, node_path.join)(path, entry.name));
|
|
421
|
+
};
|
|
422
|
+
for (const path of paths) await collect(path);
|
|
423
|
+
const stamp = stampHash.digest("hex");
|
|
424
|
+
const cached = fingerprints.get(key);
|
|
425
|
+
if (cached?.stamp === stamp) return cached.rev;
|
|
426
|
+
const hash = (0, node_crypto.createHash)("sha1");
|
|
427
|
+
const walk = async (path) => {
|
|
428
|
+
const info = await (0, node_fs_promises.stat)(path).catch(() => void 0);
|
|
429
|
+
if (info === void 0) return;
|
|
430
|
+
if (!info.isDirectory()) {
|
|
431
|
+
hash.update(`${(0, node_path.relative)(paths[0] ?? path, path).split(node_path.sep).join("/")}:${String(info.size)}`);
|
|
432
|
+
hash.update(await (0, node_fs_promises.readFile)(path));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const entries = await (0, node_fs_promises.readdir)(path, { withFileTypes: true }).catch(() => []);
|
|
436
|
+
for (const entry of [...entries].sort((left, right) => left.name.localeCompare(right.name))) await walk((0, node_path.join)(path, entry.name));
|
|
437
|
+
};
|
|
438
|
+
for (const path of paths) await walk(path);
|
|
439
|
+
const rev = hash.digest("hex").slice(0, 16);
|
|
440
|
+
fingerprints.set(key, {
|
|
441
|
+
stamp,
|
|
442
|
+
rev
|
|
443
|
+
});
|
|
444
|
+
return rev;
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* 说明符 → 联邦地址(同步:`paths` 回调与 vite 的 `resolveId` 都要求同步可答)。
|
|
448
|
+
*
|
|
449
|
+
* 两端共用这一份:产物里外置的 `import` / `require`,与 dev 侧源码里的裸说明符——同一条链、同一种地址。
|
|
450
|
+
*
|
|
451
|
+
* `?v=` 是**构建世代**({@link BUNDLER_REV},与 vite 同名的 query 参数):所有地址一次会话内稳定一致——同一个包**只有一个 URL**
|
|
452
|
+
* (多一个 URL 就多一份模块实例)。内容变了**不换 URL**,靠 `cache-control: no-cache` + `ETag` 协商
|
|
453
|
+
* ——vite dev 对它自己的模块也是这套。
|
|
454
|
+
*/
|
|
455
|
+
/**
|
|
456
|
+
* **本包视角**的可外置包与版本:从引它的那个包的清单出发,用 `findPackageJSON` 按 node 的解析找。
|
|
457
|
+
*
|
|
458
|
+
* 两点都要紧:
|
|
459
|
+
* - **按包解析**:不同包的依赖树不同(peer 分叉时同一个名字可以是不同版本),全局一张表必然猜错;
|
|
460
|
+
* - **不碰布局**:`node:module` 的解析对 hoisted / `.pnpm` / 实体目录一视同仁——我们不该知道 pnpm 把包放哪儿。
|
|
461
|
+
*
|
|
462
|
+
* 候选是"这个包声明的依赖"(`dependencies` + `peerDependencies`):声明过、又能解析到,才谈得上外置;
|
|
463
|
+
* 解析不到的(可选依赖、纯 host 包)不列,产物里内联或由上层如实报错。
|
|
464
|
+
*
|
|
465
|
+
* @param manifestPath - 本包清单的绝对路径。
|
|
466
|
+
* @returns 包名 → 版本(供 {@link urlOf} 写地址)。
|
|
467
|
+
*/
|
|
468
|
+
/** 本包视角版本表的缓存:**键是"包 + 版本"**(与产物缓存目录 `cacheKeyOf` 同一形态)——同一个包的同一个版本只解析一次。 */
|
|
469
|
+
const resolvableCache = /* @__PURE__ */ new Map();
|
|
470
|
+
/**
|
|
471
|
+
* 带缓存的前者:按"包 + 版本"复用解析结果。
|
|
472
|
+
*
|
|
473
|
+
* @param key - `cacheKeyOf(name, version)`(与产物目录名同一串)。
|
|
474
|
+
* @param manifestPath - 本包清单的绝对路径。
|
|
475
|
+
* @returns 包名 → 版本。
|
|
476
|
+
*/
|
|
477
|
+
async function resolvableVersionsFor(key, manifestPath) {
|
|
478
|
+
const cached = resolvableCache.get(key);
|
|
479
|
+
if (cached !== void 0) return await cached;
|
|
480
|
+
const computed = resolvableVersionsOf(manifestPath);
|
|
481
|
+
resolvableCache.set(key, computed);
|
|
482
|
+
return await computed;
|
|
483
|
+
}
|
|
484
|
+
async function resolvableVersionsOf(manifestPath) {
|
|
485
|
+
const versions = /* @__PURE__ */ new Map();
|
|
486
|
+
if (manifestPath === void 0) return versions;
|
|
487
|
+
const here = (0, node_url.pathToFileURL)(manifestPath).href;
|
|
488
|
+
const manifest = JSON.parse(await (0, node_fs_promises.readFile)(manifestPath, "utf8").catch(() => "{}"));
|
|
489
|
+
const names = /* @__PURE__ */ new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.peerDependencies ?? {}).filter((name) => manifest.peerDependenciesMeta?.[name]?.optional !== true)]);
|
|
490
|
+
for (const name of names) {
|
|
491
|
+
let found;
|
|
492
|
+
try {
|
|
493
|
+
found = (0, node_module.findPackageJSON)(name, here);
|
|
494
|
+
} catch {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
if (found === void 0) continue;
|
|
498
|
+
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(found, "utf8").catch(() => "{}"));
|
|
499
|
+
if (typeof parsed.version === "string") versions.set(name, parsed.version);
|
|
500
|
+
}
|
|
501
|
+
return versions;
|
|
502
|
+
}
|
|
503
|
+
function urlOf(specifier, tables) {
|
|
504
|
+
const bare = packageNameOf(specifier) || specifier;
|
|
505
|
+
const version = tables.versions?.get(bare) ?? "0.0.0";
|
|
506
|
+
const subpath = specifier === bare ? "" : specifier.slice(bare.length + 1);
|
|
507
|
+
return depsUrlOf(specifier, version, `${subpath !== "" ? /\.[a-z0-9]+$/iu.test(subpath) ? subpath : entryFileNameOf(subpath) : entryFileNameOf(tables.clientRows?.has(bare) === true ? "client" : "index")}?v=${BUNDLER_REV}`);
|
|
508
|
+
}
|
|
509
|
+
/**
|
|
510
|
+
* 依赖里的 `process.env`。
|
|
511
|
+
*
|
|
512
|
+
* 顶层的 `define` 只作用于我们的源码面:`node_modules` 里的包直接进产物,而 `immer@10` 的
|
|
513
|
+
* `dist/immer.mjs` 在模块顶层就读 `process.env.NODE_ENV`——浏览器里没有 `process`,一个
|
|
514
|
+
* `process is not defined` 会把整份产物带下去。
|
|
515
|
+
*
|
|
516
|
+
* **只替换 `process.env` 系列**:裸的 `process` 不能碰——它可能只是个普通标识符(`zod` 里就有一个叫
|
|
517
|
+
* `process` 的函数,替换掉直接把源码变成语法错误)。
|
|
518
|
+
*/
|
|
519
|
+
function browserProcessPlugin() {
|
|
520
|
+
const replace = (code) => code.replaceAll(/\bprocess\.env\.NODE_ENV\b/gu, JSON.stringify("development")).replaceAll(/\bprocess\.env\b/gu, "({ env: {} })");
|
|
521
|
+
return {
|
|
522
|
+
name: "dsh-browser-process",
|
|
523
|
+
enforce: "pre",
|
|
524
|
+
transform(code) {
|
|
525
|
+
if (!code.includes("process.env")) return null;
|
|
526
|
+
return {
|
|
527
|
+
code: replace(code),
|
|
528
|
+
map: null
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* CJS 入口的**具名导出包装**(`react` 这类)。
|
|
535
|
+
*
|
|
536
|
+
* `react` 的入口是 `module.exports = require('./cjs/react.development.js')`,rolldown 静态分析不出具名
|
|
537
|
+
* 导出,产物只剩 `export default …`——而浏览器侧要 `import { Fragment } from "react"`,于是**整个 import
|
|
538
|
+
* 阶段就失败**(入口一行都执行不了)。ESM 的导出必须**静态可枚举**(不能 `export * from` 一个运行期对象),
|
|
539
|
+
* 所以这里求值入口拿到它的键,生成一份把它们写成 `export const` 的包装文件,拿包装文件当入口。
|
|
540
|
+
*
|
|
541
|
+
* @returns 包装文件路径;入口不是 CJS、或求值拿不到键时 undefined(保持原样)。
|
|
542
|
+
*/
|
|
543
|
+
async function cjsShimOf(options) {
|
|
544
|
+
const source = await (0, node_fs_promises.readFile)(options.entry, "utf8").catch(() => "");
|
|
545
|
+
if (!(/\.(?:cjs|js)$/u.test(options.entry) && /(?:^|\n)\s*(?:module\.exports\s*=|exports\.[A-Za-z_$][A-Za-z0-9_$]*\s*=)/u.test(source))) return void 0;
|
|
546
|
+
let keys;
|
|
547
|
+
try {
|
|
548
|
+
const loaded = (0, node_module.createRequire)(options.entry)(options.entry);
|
|
549
|
+
keys = Object.keys(loaded ?? {});
|
|
550
|
+
} catch {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
const named = keys.filter((key) => key !== "default" && /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key));
|
|
554
|
+
if (named.length === 0) return void 0;
|
|
555
|
+
const file = (0, node_path.join)(options.outDir, `${options.exportName.replaceAll("/", "+")}.cjs-shim.mjs`);
|
|
556
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(file), { recursive: true });
|
|
557
|
+
await (0, node_fs_promises.writeFile)(file, [
|
|
558
|
+
`import mod from ${JSON.stringify(options.entry)};`,
|
|
559
|
+
"const __cjs = mod ?? {};",
|
|
560
|
+
"export default __cjs;",
|
|
561
|
+
...named.map((key) => `export const ${key} = __cjs[${JSON.stringify(key)}];`),
|
|
562
|
+
""
|
|
563
|
+
].join("\n"), "utf8");
|
|
564
|
+
options.log?.(`[bundle-cjs] ${options.name}/${options.exportName}: 补 ${String(named.length)} 个具名导出\n`);
|
|
565
|
+
return file;
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* CSS 入口的包装:**样式文件不能直接当入口**——vite 的库模式固定 `cssCodeSplit: false`,遇到
|
|
569
|
+
* `rolldownOptions.input` 里有 `.css` 直接报错。包一层 side-effect import,让样式插件按它一贯的方式
|
|
570
|
+
* 把它编成"注入 `<style>`"的模块。
|
|
571
|
+
*
|
|
572
|
+
* @returns 包装文件路径;入口不是 CSS 时 undefined。
|
|
573
|
+
*/
|
|
574
|
+
async function cssShimOf(options) {
|
|
575
|
+
if (!options.entry.endsWith(".css")) return void 0;
|
|
576
|
+
const file = (0, node_path.join)(options.outDir, `${options.exportName.replaceAll("/", "+")}.css-shim.mjs`);
|
|
577
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(file), { recursive: true });
|
|
578
|
+
await (0, node_fs_promises.writeFile)(file, `import ${JSON.stringify(options.entry)};\n`, "utf8");
|
|
579
|
+
return file;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* 确保该包的产物在(指纹没变、且**这次要的出口**都在就直接用)。
|
|
583
|
+
*
|
|
584
|
+
* 缓存是**按包聚合**的(一个包一个目录、出口各有自己的产物文件),而构建**按请求触发单出口**——
|
|
585
|
+
* 所以命中判据必须带上"这次要的出口在不在":只比指纹会把"先请求 `zustand/middleware`、再请求
|
|
586
|
+
* `zustand/vanilla`"判成命中,第二个出口就 404 了。指纹变了才丢掉旧目录重来,否则增量合并。
|
|
587
|
+
*
|
|
588
|
+
* **同一个包的构建排队串行**:产物目录是包级共享的(还会被重建时清理),并发跑会互相删掉对方的产物——
|
|
589
|
+
* 首屏那几十个请求是同时在飞的,`zustand/vanilla` 与 `zustand/shallow` 撞在一起就丢产物、页面 404。
|
|
590
|
+
*
|
|
591
|
+
* @param options - 见 {@link BundleOptions}。
|
|
592
|
+
* @returns 产物映射;`input` 为空时 undefined。
|
|
593
|
+
*/
|
|
594
|
+
/**
|
|
595
|
+
* 摆正 **cwd** 之后再进构建。
|
|
596
|
+
*
|
|
597
|
+
* rolldown 的 oxc 转译是**按 cwd 往上找 tsconfig** 拿 `experimentalDecorators` 的,而这条链跑在宿主
|
|
598
|
+
* 进程里——cwd 是宿主的(dev 是工作区、打包是 profile)。不摆正,上游用 cordis 装饰器的包就会在产物里
|
|
599
|
+
* 留着 `@Remote(...)`,浏览器报 `SyntaxError`、入口整条不执行(这是"不同工作目录行为不一致"里最隐蔽的一处)。
|
|
600
|
+
* 跑完恢复,别把进程的 cwd 改坏了。
|
|
601
|
+
*/
|
|
602
|
+
async function bundlePackage(options) {
|
|
603
|
+
const previousCwd = process.cwd();
|
|
604
|
+
if (previousCwd !== options.repoRoot) process.chdir(options.repoRoot);
|
|
605
|
+
try {
|
|
606
|
+
return await bundlePackageInRoot(options);
|
|
607
|
+
} finally {
|
|
608
|
+
if (process.cwd() !== previousCwd) process.chdir(previousCwd);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
async function bundlePackageInRoot(options) {
|
|
612
|
+
const key = cacheKeyOf(options.name, options.version);
|
|
613
|
+
const task = (building.get(key) ?? Promise.resolve()).catch(() => void 0).then(() => withBuildSlot(() => bundlePackageOnce(options, key)));
|
|
614
|
+
building.set(key, task);
|
|
615
|
+
try {
|
|
616
|
+
return await withTimeout(task, BUILD_TIMEOUT_MS, `${options.name}@${options.version} 构建超时`);
|
|
617
|
+
} finally {
|
|
618
|
+
if (building.get(key) === task) building.delete(key);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 同时在跑的构建数上限。
|
|
623
|
+
*
|
|
624
|
+
* 每次联邦构建都是一次完整的 vite build(自建实例、加载插件、跑 rolldown),首屏那两百多个包**一起上**
|
|
625
|
+
* 只会互相拖慢、把内存吃满——排队反而更快。要调就设 `DSH_BUNDLE_CONCURRENCY`。
|
|
626
|
+
*/
|
|
627
|
+
const MAX_CONCURRENT_BUILDS = Math.max(1, Number(process.env["DSH_BUNDLE_CONCURRENCY"] ?? 8));
|
|
628
|
+
let runningBuilds = 0;
|
|
629
|
+
const buildQueue = [];
|
|
630
|
+
/** 排队/构建的等待上限:等它做完,但不无限等(卡死的构建要如实报出来,而不是让请求挂在 pending)。 */
|
|
631
|
+
const BUILD_TIMEOUT_MS = Math.max(1e3, Number(process.env["DSH_BUNDLE_TIMEOUT_MS"] ?? 18e4));
|
|
632
|
+
/** 给一个 promise 加超时;超时如实抛,不返回兜底值。 */
|
|
633
|
+
async function withTimeout(work, ms, subject) {
|
|
634
|
+
let timer;
|
|
635
|
+
try {
|
|
636
|
+
return await Promise.race([work, new Promise((_resolve, reject) => {
|
|
637
|
+
timer = setTimeout(() => {
|
|
638
|
+
reject(/* @__PURE__ */ new Error(`${subject}(超过 ${String(ms)}ms)`));
|
|
639
|
+
}, ms);
|
|
640
|
+
})]);
|
|
641
|
+
} finally {
|
|
642
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async function withBuildSlot(work) {
|
|
646
|
+
if (runningBuilds >= MAX_CONCURRENT_BUILDS) await new Promise((resolve) => buildQueue.push(resolve));
|
|
647
|
+
runningBuilds += 1;
|
|
648
|
+
try {
|
|
649
|
+
return await work();
|
|
650
|
+
} finally {
|
|
651
|
+
runningBuilds -= 1;
|
|
652
|
+
buildQueue.shift()?.();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
/** 排队中/在跑的包级构建(键是缓存目录名)。 */
|
|
656
|
+
const building = /* @__PURE__ */ new Map();
|
|
657
|
+
async function bundlePackageOnce(options, key) {
|
|
658
|
+
const wanted = Object.keys(options.input);
|
|
659
|
+
options.log?.(`[bundle-in] ${options.name}: input=${wanted.join(",")} cache=${options.cacheRoot}\n`);
|
|
660
|
+
if (wanted.length === 0) return void 0;
|
|
661
|
+
const outDir = (0, node_path.join)(options.cacheRoot, key);
|
|
662
|
+
const manifestPath = (0, node_path.join)(outDir, ".files.json");
|
|
663
|
+
options.log?.(`[bundle-rev] ${options.name}: hash-start watch=${options.watch.join(",")}\n`);
|
|
664
|
+
const rev = `${BUNDLER_REV}:${await fingerprint(options.watch)}`;
|
|
665
|
+
options.log?.(`[bundle-rev] ${options.name}: hash=${rev}\n`);
|
|
666
|
+
const cached = JSON.parse(await (0, node_fs_promises.readFile)(manifestPath, "utf8").catch(() => "{}"));
|
|
667
|
+
const fresh = cached.rev === rev;
|
|
668
|
+
const files = fresh ? { ...cached.files } : {};
|
|
669
|
+
if (fresh && wanted.every((name) => name in files)) return {
|
|
670
|
+
rev,
|
|
671
|
+
files: new Map(Object.entries(files))
|
|
672
|
+
};
|
|
673
|
+
const resolvable = await resolvableVersionsFor(key, options.manifestOf);
|
|
674
|
+
const plugins = [
|
|
675
|
+
{
|
|
676
|
+
name: "dsh-alias",
|
|
677
|
+
resolveId: (source) => {
|
|
678
|
+
for (const { find, replacement } of options.alias) if (typeof find === "string" ? source.startsWith(find) : find.test(source)) return source.replace(find, replacement);
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
browserProcessPlugin(),
|
|
682
|
+
(0, _morlay_dsh_cordis_assets_styles.cssInlinePlugins)({
|
|
683
|
+
name: key,
|
|
684
|
+
assets: "threshold",
|
|
685
|
+
assetUrl: (file) => `${CLIENT_MODULES_PREFIX}/${key}/${file}`
|
|
686
|
+
}),
|
|
687
|
+
(0, _morlay_dsh_cordis_assets_assets.clientAssetPlugin)({ assets: "threshold" }),
|
|
688
|
+
(0, vite.esmExternalRequirePlugin)({ external: [...resolvable.keys()].filter((name) => !_morlay_dsh_cordis_assets_assets.NODE_BUILTIN.test(name)).map((name) => barePackageRegExp(name)) })
|
|
689
|
+
];
|
|
690
|
+
const startedAt = Date.now();
|
|
691
|
+
options.log?.(`[bundle-build] ${options.name}: rolldown start\n`);
|
|
692
|
+
const input = {};
|
|
693
|
+
for (const [exportName, entry] of Object.entries(options.input)) input[exportName] = await cjsShimOf({
|
|
694
|
+
name: options.name,
|
|
695
|
+
exportName,
|
|
696
|
+
entry,
|
|
697
|
+
...options.log === void 0 ? {} : { log: options.log },
|
|
698
|
+
outDir
|
|
699
|
+
}) ?? await cssShimOf({
|
|
700
|
+
exportName,
|
|
701
|
+
entry,
|
|
702
|
+
outDir
|
|
703
|
+
}) ?? entry;
|
|
704
|
+
const bundle = await (0, rolldown.rolldown)({
|
|
705
|
+
input,
|
|
706
|
+
plugins,
|
|
707
|
+
...options.tsconfig === void 0 ? {} : { resolve: { tsconfigFilename: options.tsconfig } },
|
|
708
|
+
transform: {
|
|
709
|
+
target: [
|
|
710
|
+
"chrome111",
|
|
711
|
+
"edge111",
|
|
712
|
+
"firefox114",
|
|
713
|
+
"safari16.4"
|
|
714
|
+
],
|
|
715
|
+
decorator: { legacy: true },
|
|
716
|
+
define: {
|
|
717
|
+
"process.env": "{ env: {} }",
|
|
718
|
+
process: "{ env: {} }"
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
const { output: items } = await bundle.generate({
|
|
723
|
+
format: "es",
|
|
724
|
+
entryFileNames: "[name].js",
|
|
725
|
+
chunkFileNames: "chunks/[name]-[hash].js",
|
|
726
|
+
assetFileNames: "assets/[name]-[hash][extname]",
|
|
727
|
+
paths: (id) => urlOf(id, {
|
|
728
|
+
versions: resolvable,
|
|
729
|
+
...options.clientRows === void 0 ? {} : { clientRows: options.clientRows }
|
|
730
|
+
})
|
|
731
|
+
});
|
|
732
|
+
await bundle.close();
|
|
733
|
+
options.log?.(`[bundle-build] ${options.name}: rolldown done ms=${String(Date.now() - startedAt)}\n`);
|
|
734
|
+
if (!items.some((item) => item.type === "chunk")) {
|
|
735
|
+
options.log?.(`[bundle] ${options.name}: 输出为空(input=${wanted.join(",")} outputs=${String(items.length)})\n`);
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
await (0, node_fs_promises.mkdir)(outDir, { recursive: true });
|
|
739
|
+
for (const item of items) {
|
|
740
|
+
if (item.type !== "chunk") continue;
|
|
741
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)((0, node_path.join)(outDir, item.fileName)), { recursive: true });
|
|
742
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outDir, item.fileName), item.code, "utf8");
|
|
743
|
+
files[item.isEntry ? item.name : item.fileName] = `${key}/${item.fileName}`;
|
|
744
|
+
}
|
|
745
|
+
for (const asset of items) {
|
|
746
|
+
if (asset.type !== "asset") continue;
|
|
747
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)((0, node_path.join)(outDir, asset.fileName)), { recursive: true });
|
|
748
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(outDir, asset.fileName), asset.source ?? "");
|
|
749
|
+
files[asset.fileName] = `${key}/${asset.fileName}`;
|
|
750
|
+
}
|
|
751
|
+
await (0, node_fs_promises.writeFile)(manifestPath, `${JSON.stringify({
|
|
752
|
+
rev,
|
|
753
|
+
files
|
|
754
|
+
}, null, 2)}\n`, "utf8");
|
|
755
|
+
return {
|
|
756
|
+
rev,
|
|
757
|
+
files: new Map(Object.entries(files))
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
/** 三方的入口:按 node 的解析找(pnpm 的软链由它解开)。 */
|
|
761
|
+
async function dependencyEntry(from, name) {
|
|
762
|
+
const require$1 = (0, node_module.createRequire)(from);
|
|
763
|
+
let entry;
|
|
764
|
+
try {
|
|
765
|
+
entry = require$1.resolve(name);
|
|
766
|
+
} catch {
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
const manifest = await packageJsonNear(entry);
|
|
770
|
+
const parsed = JSON.parse(await (0, node_fs_promises.readFile)(manifest, "utf8").catch(() => "{}"));
|
|
771
|
+
return {
|
|
772
|
+
entry,
|
|
773
|
+
version: parsed.version ?? "0.0.0"
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/** 从入口文件向上找它所属包的 `package.json`。 */
|
|
777
|
+
async function packageJsonNear(file) {
|
|
778
|
+
let directory = (0, node_path.dirname)(file);
|
|
779
|
+
for (;;) {
|
|
780
|
+
const candidate = (0, node_path.join)(directory, "package.json");
|
|
781
|
+
if (await (0, node_fs_promises.readFile)(candidate, "utf8").then(() => true, () => false)) return candidate;
|
|
782
|
+
const parent = (0, node_path.dirname)(directory);
|
|
783
|
+
if (parent === directory) return candidate;
|
|
784
|
+
directory = parent;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
//#endregion
|
|
788
|
+
//#region src/dirs.ts
|
|
789
|
+
/** 本包自己的根(页面的骨架、`node-stub` 与依赖解析的兜底基准都从它推)。 */
|
|
790
|
+
function packageRoot() {
|
|
791
|
+
return (0, node_path.dirname)((0, node_url.fileURLToPath)(new URL("../package.json", require("url").pathToFileURL(__filename).href)));
|
|
792
|
+
}
|
|
793
|
+
/** 页面与入口所在的目录(vite 的 root):本包的 `app-boot`。 */
|
|
794
|
+
function appBootDir() {
|
|
795
|
+
return (0, node_path.join)(packageRoot(), "app-boot");
|
|
796
|
+
}
|
|
797
|
+
/** 本包的清单(依赖解析的一个基准:这条链的运行期依赖声明在它里面)。 */
|
|
798
|
+
function packageManifestPath() {
|
|
799
|
+
return (0, node_path.join)(packageRoot(), "package.json");
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* 装配根(profileDir):这份部署装配出来的那一格。
|
|
803
|
+
*
|
|
804
|
+
* dev 是工作区里的 app 目录(`dshdev` 给的 `DSH_DESKTOP_DEV_PROFILE_DIR`),bundle 是宿主进程的 cwd
|
|
805
|
+
* (壳把宿主的 cwd 设成 `<dataDir>/profiles/<name>`)。
|
|
806
|
+
*
|
|
807
|
+
* @returns 第一个带 `package.json` 的候选取值;都没有时退回 cwd。
|
|
808
|
+
*/
|
|
809
|
+
async function profileDir() {
|
|
810
|
+
const candidates = [process.env["DSH_DESKTOP_DEV_PROFILE_DIR"], process.cwd()].filter((value) => value !== void 0 && value !== "");
|
|
811
|
+
for (const candidate of candidates) if (await (0, node_fs_promises.readFile)((0, node_path.join)(candidate, "package.json"), "utf8").then(() => true, () => false)) return candidate;
|
|
812
|
+
return process.cwd();
|
|
813
|
+
}
|
|
814
|
+
/** 装配根的清单:`<profileDir>/package.json`(pnpm 从这里解析到装配行与工作区的包)。 */
|
|
815
|
+
async function profileManifestPath() {
|
|
816
|
+
return (0, node_path.join)(await profileDir(), "package.json");
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* 解析根:依赖解析、watch、缓存的基准。
|
|
820
|
+
*
|
|
821
|
+
* dev 再向上找**工作区根**(`node_modules` 与 lockfile 在那儿);bundle 的装配根本身就带自己的
|
|
822
|
+
* `node_modules`,就是它。
|
|
823
|
+
*/
|
|
824
|
+
async function resolutionRoot() {
|
|
825
|
+
resolutionRootPromise ??= (async () => {
|
|
826
|
+
const profile = await profileDir();
|
|
827
|
+
return isDevForm() ? await workspaceRootOf(profile) ?? profile : profile;
|
|
828
|
+
})();
|
|
829
|
+
return await resolutionRootPromise;
|
|
830
|
+
}
|
|
831
|
+
let resolutionRootPromise;
|
|
832
|
+
/**
|
|
833
|
+
* 数据根(`$DSH_HOME`):会话、设置、附件、日志。
|
|
834
|
+
*
|
|
835
|
+
* dev 缺省是 `<装配根>/.dsh-store`(`dshdev` 会显式给值),bundle 由壳按 app 的 `dsh.desktop.dataDir`
|
|
836
|
+
* 解析后经环境传进来。
|
|
837
|
+
*/
|
|
838
|
+
async function dataDir() {
|
|
839
|
+
return process.env["DSH_HOME"] ?? (0, node_path.join)(await profileDir(), ".dsh-store");
|
|
840
|
+
}
|
|
841
|
+
/** 解析根的清单:`<解析根>/package.json`(依赖解析的另一个基准)。 */
|
|
842
|
+
async function resolutionManifestPath() {
|
|
843
|
+
return (0, node_path.join)(await resolutionRoot(), "package.json");
|
|
844
|
+
}
|
|
845
|
+
/** 这条链自己的缓存目录(联邦产物与聚合结果,与 vite 的 `.vite` 并列)。 */
|
|
846
|
+
async function cacheDir() {
|
|
847
|
+
return (0, node_path.join)(await resolutionRoot(), CLIENT_MODULES_CACHE);
|
|
848
|
+
}
|
|
849
|
+
const RESOLVED_ROWS = `\0dsh-client-rows`;
|
|
850
|
+
/**
|
|
851
|
+
* 依赖预打包的入口名。vite 的 `optimizeDeps.entries` 期望"文件路径",而我们手上是包名与源码路径的
|
|
852
|
+
* 映射,所以用一个小技巧:把入口写成**虚拟模块**再让 vite 把它当 discovery 的种子——见 `configResolved`
|
|
853
|
+
* 里的 `optimizeDeps.include`。
|
|
854
|
+
*/
|
|
855
|
+
/** 从某个文件向上找包根(有 `package.json` 的那一层)。 */
|
|
856
|
+
async function packageRootOfFile(file) {
|
|
857
|
+
let directory = (0, node_path.dirname)(file);
|
|
858
|
+
for (;;) {
|
|
859
|
+
if (await (0, node_fs_promises.readFile)((0, node_path.join)(directory, "package.json"), "utf8").then(() => true, () => false)) return directory;
|
|
860
|
+
const parent = (0, node_path.dirname)(directory);
|
|
861
|
+
if (parent === directory) return void 0;
|
|
862
|
+
directory = parent;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* 「有 client 先用 client」:给定一条**从 client 图里发起**的相对引用,返回它的 client 变体(没有则 undefined)。
|
|
867
|
+
*
|
|
868
|
+
* 上游的包是同构的:`src/foo.ts` 与 `src/client/foo.ts` 成对,`src/client/index.ts` 会值导入 `../foo.ts`
|
|
869
|
+
* 这类共享文件。若该共享文件在 client 侧有对应实现,就用它——这与 node 的 `browser` 字段、打包器的
|
|
870
|
+
* `browser` 条件是同一件事,只是上游的 `exports` 没有为每个子路径声明 browser 条件。
|
|
871
|
+
*
|
|
872
|
+
* @param source - 引用说明符(相对路径)。
|
|
873
|
+
* @param importer - 发起引用的文件(必在某个包的 `src/client/` 下)。
|
|
874
|
+
* @returns client 变体的绝对路径,或 undefined(没有变体、或引用跨出了本包)。
|
|
875
|
+
*/
|
|
876
|
+
async function clientVariantOf(source, importer) {
|
|
877
|
+
const marker = `${node_path.sep}src${node_path.sep}client${node_path.sep}`;
|
|
878
|
+
const at = importer.indexOf(marker);
|
|
879
|
+
if (at < 0) return void 0;
|
|
880
|
+
const packageSrc = importer.slice(0, at + `${node_path.sep}src`.length);
|
|
881
|
+
const target = (0, node_path.resolve)((0, node_path.dirname)(importer), source);
|
|
882
|
+
if (target.includes(marker)) return void 0;
|
|
883
|
+
const relativeFromSrc = (0, node_path.relative)(packageSrc, target);
|
|
884
|
+
if (relativeFromSrc.startsWith("..")) return void 0;
|
|
885
|
+
const variant = (0, node_path.join)(packageSrc, "client", relativeFromSrc);
|
|
886
|
+
return await (0, node_fs_promises.readFile)(variant, "utf8").then(() => true, () => false) ? variant : void 0;
|
|
887
|
+
}
|
|
888
|
+
function dshClientRowsPlugin(options) {
|
|
889
|
+
let excludedCache;
|
|
890
|
+
const excludedOf = async () => {
|
|
891
|
+
if (excludedCache === void 0) excludedCache = await devExcluded(await profileDir());
|
|
892
|
+
return excludedCache;
|
|
893
|
+
};
|
|
894
|
+
(0, node_module.createRequire)((0, node_path.join)(options.repoRoot, "package.json"));
|
|
895
|
+
return {
|
|
896
|
+
name: "dsh-client-rows",
|
|
897
|
+
enforce: "pre",
|
|
898
|
+
config() {
|
|
899
|
+
return { resolve: { conditions: [
|
|
900
|
+
"browser",
|
|
901
|
+
"import",
|
|
902
|
+
"module",
|
|
903
|
+
"default"
|
|
904
|
+
] } };
|
|
905
|
+
},
|
|
906
|
+
async resolveId(source, importer) {
|
|
907
|
+
if (source === "dsh-client-rows") return RESOLVED_ROWS;
|
|
908
|
+
if (source.startsWith(`/@client-modules/`)) return {
|
|
909
|
+
id: source,
|
|
910
|
+
external: "absolute"
|
|
911
|
+
};
|
|
912
|
+
if (source.startsWith("@/")) return null;
|
|
913
|
+
if (source.startsWith("/") && options.resolvePluginUrl !== void 0) {
|
|
914
|
+
const file = await options.resolvePluginUrl(source.split("?")[0] ?? source);
|
|
915
|
+
if (file !== void 0) return file;
|
|
916
|
+
}
|
|
917
|
+
if (source.startsWith("\0")) return null;
|
|
918
|
+
if (importer !== void 0 && importer.includes(`${node_path.sep}src${node_path.sep}client${node_path.sep}`)) {
|
|
919
|
+
const variant = await clientVariantOf(source, importer);
|
|
920
|
+
if (variant !== void 0) return variant;
|
|
921
|
+
}
|
|
922
|
+
if (importer === void 0 || source.startsWith(".") || source.startsWith("/")) return null;
|
|
923
|
+
const fromFile = importer === void 0 || importer.startsWith("\0") ? void 0 : importer.startsWith("/") ? (0, node_path.join)(options.repoRoot, importer) : importer;
|
|
924
|
+
if (fromFile !== void 0) {
|
|
925
|
+
const bare = packageNameOf(source);
|
|
926
|
+
if (bare !== "") {
|
|
927
|
+
const pkg = await packageRootOf(fromFile, bare);
|
|
928
|
+
if (pkg !== void 0 && !pkg.root.includes("node_modules") && !isAggregated(bare, await excludedOf())) {
|
|
929
|
+
const subpath = source === bare ? void 0 : source.slice(bare.length + 1);
|
|
930
|
+
const half = await clientHalfOf(pkg.root, subpath);
|
|
931
|
+
if (half !== void 0) return half;
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
if (options.depsUrl !== void 0) {
|
|
936
|
+
const url = await options.depsUrl(source, importer);
|
|
937
|
+
if (url !== void 0) return {
|
|
938
|
+
id: url,
|
|
939
|
+
external: true
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
const rows = await options.rows();
|
|
943
|
+
const shared = await options.shared?.() ?? /* @__PURE__ */ new Map();
|
|
944
|
+
const known = rows.get(source) ?? shared.get(source);
|
|
945
|
+
if (known !== void 0 && known !== source) return known;
|
|
946
|
+
const slash = source.startsWith("@") ? source.indexOf("/", source.indexOf("/") + 1) : source.indexOf("/");
|
|
947
|
+
if (slash > 0) {
|
|
948
|
+
const pkgName = source.slice(0, slash);
|
|
949
|
+
const subpath = source.slice(slash + 1);
|
|
950
|
+
const rowPath = rows.get(pkgName) ?? shared.get(pkgName);
|
|
951
|
+
if (rowPath !== void 0) {
|
|
952
|
+
const rowRoot = await packageRootOfFile(rowPath);
|
|
953
|
+
const target = rowRoot === void 0 ? void 0 : await exportTargetOf(rowRoot, subpath);
|
|
954
|
+
if (target !== void 0) return target;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return null;
|
|
958
|
+
},
|
|
959
|
+
async load(id) {
|
|
960
|
+
if (id !== RESOLVED_ROWS) return null;
|
|
961
|
+
const rows = await options.rows();
|
|
962
|
+
const shared = await options.shared?.() ?? /* @__PURE__ */ new Map();
|
|
963
|
+
const imports = [];
|
|
964
|
+
const entries = [];
|
|
965
|
+
let index = 0;
|
|
966
|
+
for (const [pkg, file] of [...shared, ...rows]) {
|
|
967
|
+
const alias = `m${String(index)}`;
|
|
968
|
+
const specifier = file === pkg ? await options.depsUrl?.(pkg) ?? file : file;
|
|
969
|
+
imports.push(`import * as ${alias} from ${JSON.stringify(specifier)};`);
|
|
970
|
+
entries.push(` { id: ${JSON.stringify(pkg)}, module: ${alias} },`);
|
|
971
|
+
index += 1;
|
|
972
|
+
}
|
|
973
|
+
return [
|
|
974
|
+
...imports,
|
|
975
|
+
"",
|
|
976
|
+
"export default [",
|
|
977
|
+
...entries,
|
|
978
|
+
"];",
|
|
979
|
+
""
|
|
980
|
+
].join("\n");
|
|
981
|
+
},
|
|
982
|
+
configureServer(devServer) {
|
|
983
|
+
options.onServer?.(devServer);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
//#endregion
|
|
988
|
+
//#region src/serve-vite.ts
|
|
989
|
+
function rowsOf(source) {
|
|
990
|
+
return source.rows();
|
|
991
|
+
}
|
|
992
|
+
/** 起一个 vite dev server(middleware 模式),并把 `/plugins` 前缀下的请求交给它。 */
|
|
993
|
+
/** vite 的 HMR WebSocket 路径:专有路径,避免与上游自身的 upgrade 路由抢。 */
|
|
994
|
+
const HMR_PATH = "/__dsh_hmr";
|
|
995
|
+
async function startViteDev(ctx, options) {
|
|
996
|
+
const trace = traceOf(ctx, "vite");
|
|
997
|
+
const { repoRoot, base } = options;
|
|
998
|
+
/**
|
|
999
|
+
* 页面与入口所在的真实目录(vite 的 root)。
|
|
1000
|
+
*
|
|
1001
|
+
* 按**本包自己的位置**推,不从 `repoRoot` 拼:打包形态下 `repoRoot` 是 profile 目录
|
|
1002
|
+
* (`$DSH_HOME/profiles/desktop`),从它拼出的 `packages/client/…/app-boot` 并不存在——vite 的 root
|
|
1003
|
+
* 一空,`/index.html` 404、入口也转译不了,页面就只剩一具空壳。这条事实与 `index.ts` 的 `appRoot`
|
|
1004
|
+
* 同源(那里也从包根推)。
|
|
1005
|
+
*/
|
|
1006
|
+
const appRoot = (0, node_url.fileURLToPath)(new URL("../app-boot", require("url").pathToFileURL(__filename).href));
|
|
1007
|
+
/** 页面所在的端口(HMR 的 WebSocket 必须与页面**同源同端口**,否则客户端连不上)。 */
|
|
1008
|
+
const pagePort = (() => {
|
|
1009
|
+
const address = options.httpServer?.address();
|
|
1010
|
+
if (address !== null && address !== void 0 && typeof address === "object") return address.port;
|
|
1011
|
+
const fromEnv = process.env["PORT"];
|
|
1012
|
+
return fromEnv === void 0 ? void 0 : Number(fromEnv);
|
|
1013
|
+
})();
|
|
1014
|
+
let current;
|
|
1015
|
+
const server = await (0, vite.createServer)({
|
|
1016
|
+
root: appRoot,
|
|
1017
|
+
base: "/",
|
|
1018
|
+
define: {
|
|
1019
|
+
"process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] ?? "development"),
|
|
1020
|
+
"process.env": "{ env: {} }",
|
|
1021
|
+
process: "{ env: {} }"
|
|
1022
|
+
},
|
|
1023
|
+
publicDir: false,
|
|
1024
|
+
appType: "mpa",
|
|
1025
|
+
configFile: false,
|
|
1026
|
+
resolve: { alias: clientModulesAlias({
|
|
1027
|
+
repoRoot,
|
|
1028
|
+
nodeStub: (0, node_path.join)(appRoot, "node-stub.ts")
|
|
1029
|
+
}) },
|
|
1030
|
+
server: {
|
|
1031
|
+
middlewareMode: true,
|
|
1032
|
+
preTransformRequests: false,
|
|
1033
|
+
...options.httpServer === void 0 ? {} : { hmr: {
|
|
1034
|
+
server: options.httpServer,
|
|
1035
|
+
path: HMR_PATH,
|
|
1036
|
+
...pagePort === void 0 ? {} : { clientPort: pagePort }
|
|
1037
|
+
} },
|
|
1038
|
+
fs: {
|
|
1039
|
+
strict: false,
|
|
1040
|
+
allow: [repoRoot]
|
|
1041
|
+
}
|
|
1042
|
+
},
|
|
1043
|
+
optimizeDeps: {
|
|
1044
|
+
disabled: true,
|
|
1045
|
+
noDiscovery: true,
|
|
1046
|
+
include: []
|
|
1047
|
+
},
|
|
1048
|
+
plugins: [dshClientRowsPlugin({
|
|
1049
|
+
repoRoot,
|
|
1050
|
+
rows: () => rowsOf(options.rows),
|
|
1051
|
+
resolvePluginUrl: options.resolvePluginUrl,
|
|
1052
|
+
depsUrl: options.depsUrl,
|
|
1053
|
+
onServer: (devServer) => {
|
|
1054
|
+
current = devServer;
|
|
1055
|
+
}
|
|
1056
|
+
})]
|
|
1057
|
+
});
|
|
1058
|
+
current = server;
|
|
1059
|
+
ctx.logger.info("client-modules: vite dev server 就绪(root=%s)", repoRoot);
|
|
1060
|
+
/** 页面入口(root=app-boot,所以就是 `/index.html`)。 */
|
|
1061
|
+
const pageEntry = "/index.html";
|
|
1062
|
+
const serve = (req, res) => {
|
|
1063
|
+
if ((req.url ?? "").split("?")[0] === "/" || (req.url ?? "").split("?")[0] === "/index.html") req.url = pageEntry;
|
|
1064
|
+
if ((req.url ?? "").startsWith(`${base}/`)) return false;
|
|
1065
|
+
try {
|
|
1066
|
+
res.on("finish", () => {
|
|
1067
|
+
if (res.statusCode < 400) return;
|
|
1068
|
+
trace("%s %s → %s", req.method ?? "", req.url ?? "", res.statusCode);
|
|
1069
|
+
});
|
|
1070
|
+
(current ?? server).middlewares(req, res, () => {
|
|
1071
|
+
trace("miss %s %s", req.method ?? "", req.url ?? "");
|
|
1072
|
+
if (!res.headersSent) res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1073
|
+
if (!res.writableEnded) res.end("client-modules: vite 没有这条路由\n");
|
|
1074
|
+
});
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
ctx.logger.error(error);
|
|
1077
|
+
if (!res.headersSent) res.writeHead(500, { "content-type": "text/plain; charset=utf-8" });
|
|
1078
|
+
if (!res.writableEnded) res.end(`dev-client: vite 处理失败:${String(error)}\n`);
|
|
1079
|
+
}
|
|
1080
|
+
return true;
|
|
1081
|
+
};
|
|
1082
|
+
return {
|
|
1083
|
+
serve,
|
|
1084
|
+
transformHtml: async (html) => await server.transformIndexHtml(`${base}/`, html),
|
|
1085
|
+
hmr: options.httpServer !== void 0
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
//#endregion
|
|
1089
|
+
//#region src/index.ts
|
|
1090
|
+
const name = "client-modules";
|
|
1091
|
+
const inject = ["loader", "webServer"];
|
|
1092
|
+
/**
|
|
1093
|
+
* 行清单:**cordis Loader 的 entries** 里声明了 `dsh.client` 的那些包——**上游 `client-modules` 的同一份
|
|
1094
|
+
* 来源**("装配的真相",任何项目/上游都通用),`disabled` 的行在这里被跳过(profile 的开关就此生效)。
|
|
1095
|
+
*
|
|
1096
|
+
* 每行给出**包名**与该包 **client 半的源文件**。包的位置用 **entry 自己的 `parent.tree.ctx.baseUrl`**
|
|
1097
|
+
* 解析(上游 `resolveSource`/`resolveMeta` 用的就是它)——从进程的 cwd 解析不到这些包:pnpm 的严格布局
|
|
1098
|
+
* 下它们只在各自消费者的 node_modules 里,profile 的运行时目录又另有一套。
|
|
1099
|
+
*
|
|
1100
|
+
* **判据是清单里的 `dsh.client` 声明**(与上游 `client-modules` 同源):光有 `src/client/` 目录不算行——
|
|
1101
|
+
* 上游有若干包只是"宿主插件 + 一份 client 半"(`@deepseek-ai/dsh-session-title` 那种),它们由真正的行在
|
|
1102
|
+
* 自己的 client 半里 import,不该出现在装配清单里;混进来就会多装配一次(`apply` 里那些"同 namespace 的
|
|
1103
|
+
* locale 已注册"的报错就是这么来的),也会在入口打出一串 `skip (no apply/default)`。
|
|
1104
|
+
*/
|
|
1105
|
+
async function clientRowsOf(loader) {
|
|
1106
|
+
const found = /* @__PURE__ */ new Map();
|
|
1107
|
+
for (const entry of loader.entries()) {
|
|
1108
|
+
if (entry.disabled === true || entry.fiber === void 0) continue;
|
|
1109
|
+
const name = entry.options?.name;
|
|
1110
|
+
if (typeof name !== "string" || name === "") continue;
|
|
1111
|
+
if (name.includes(":")) continue;
|
|
1112
|
+
if (found.has(name)) continue;
|
|
1113
|
+
const baseUrl = entry.parent?.tree?.ctx?.baseUrl;
|
|
1114
|
+
if (baseUrl === void 0) continue;
|
|
1115
|
+
const resolved = await packageRootOf(baseUrl, name);
|
|
1116
|
+
if (resolved === void 0) continue;
|
|
1117
|
+
const manifest = await manifestOf(resolved.root);
|
|
1118
|
+
const declared = manifest?.dsh?.client;
|
|
1119
|
+
if (manifest === void 0 || declared === void 0) continue;
|
|
1120
|
+
const clientPath = await clientHalfOf(resolved.root);
|
|
1121
|
+
if (clientPath === void 0) continue;
|
|
1122
|
+
found.set(name, {
|
|
1123
|
+
id: name,
|
|
1124
|
+
clientPath: `@/${(0, node_path.relative)(await resolutionRoot(), clientPath).split(node_path.sep).join("/")}`,
|
|
1125
|
+
version: manifest.version
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
return [...found.values()];
|
|
1129
|
+
}
|
|
1130
|
+
/** 打包器指纹的输入集:**只与包有关、与出口无关**(缓存按包聚合,rev 跟着出口变就等于永不命中)。 */
|
|
1131
|
+
function watchOf(root, entry) {
|
|
1132
|
+
if (root === void 0) return [entry];
|
|
1133
|
+
return root.includes("node_modules") ? [(0, node_path.join)(root, "package.json")] : [(0, node_path.join)(root, "src")];
|
|
1134
|
+
}
|
|
1135
|
+
function apply(ctx) {
|
|
1136
|
+
const trace = traceOf(ctx, "federation");
|
|
1137
|
+
const webServer = ctx.get("webServer");
|
|
1138
|
+
const appRoot = appBootDir();
|
|
1139
|
+
const logFileOf = dataDir().then((dir) => (0, node_path.join)(dir, "logs", "dev-host.log"));
|
|
1140
|
+
let resolvedLogFile;
|
|
1141
|
+
logFileOf.then((file) => {
|
|
1142
|
+
resolvedLogFile = file;
|
|
1143
|
+
});
|
|
1144
|
+
const appendLog = async (text) => {
|
|
1145
|
+
try {
|
|
1146
|
+
const logFile = await logFileOf;
|
|
1147
|
+
await (0, node_fs_promises.mkdir)((0, node_path.dirname)(logFile), { recursive: true });
|
|
1148
|
+
await (0, node_fs_promises.appendFile)(logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${text}\n`, "utf8");
|
|
1149
|
+
} catch {}
|
|
1150
|
+
};
|
|
1151
|
+
ctx.effect(() => {
|
|
1152
|
+
const onException = (error) => {
|
|
1153
|
+
appendLog(`[uncaughtException] ${error.stack ?? error.message}`);
|
|
1154
|
+
};
|
|
1155
|
+
const onRejection = (reason) => {
|
|
1156
|
+
appendLog(`[unhandledRejection] ${reason instanceof Error ? reason.stack ?? reason.message : String(reason)}`);
|
|
1157
|
+
};
|
|
1158
|
+
const onExit = (code) => {
|
|
1159
|
+
const shown = resolvedLogFile ?? "(尚未就绪)";
|
|
1160
|
+
ctx.logger.info(`dev 宿主退出:code=${String(code)},异常日志见 ${shown}\n`);
|
|
1161
|
+
};
|
|
1162
|
+
process.on("uncaughtException", onException);
|
|
1163
|
+
process.on("unhandledRejection", onRejection);
|
|
1164
|
+
process.on("exit", onExit);
|
|
1165
|
+
return () => {
|
|
1166
|
+
process.off("uncaughtException", onException);
|
|
1167
|
+
process.off("unhandledRejection", onRejection);
|
|
1168
|
+
process.off("exit", onExit);
|
|
1169
|
+
};
|
|
1170
|
+
}, "client-modules: 宿主异常日志");
|
|
1171
|
+
let rowsCache;
|
|
1172
|
+
const loader = ctx.loader;
|
|
1173
|
+
let excludedCache;
|
|
1174
|
+
const excludedOf = async () => excludedCache ??= await devExcluded(await profileDir());
|
|
1175
|
+
const sourceRowsOf = async () => {
|
|
1176
|
+
rowsCache ??= loader === void 0 ? [] : await clientRowsOf(loader);
|
|
1177
|
+
return rowsCache;
|
|
1178
|
+
};
|
|
1179
|
+
const rows = async () => await sourceRowsOf();
|
|
1180
|
+
if (webServer === void 0) throw new Error("client-modules: webServer is a required service");
|
|
1181
|
+
let fallbackBase;
|
|
1182
|
+
const rootOf = async (name, from) => {
|
|
1183
|
+
const bases = from === void 0 ? [] : [from];
|
|
1184
|
+
fallbackBase ??= await profileManifestPath();
|
|
1185
|
+
if (fallbackBase !== void 0) bases.push(fallbackBase);
|
|
1186
|
+
bases.push(packageManifestPath());
|
|
1187
|
+
for (const base of bases) {
|
|
1188
|
+
const found = await packageRootOf(base, name);
|
|
1189
|
+
if (found !== void 0) return found.root;
|
|
1190
|
+
}
|
|
1191
|
+
};
|
|
1192
|
+
/** 兜底:vite 不认的路径直接 404(dev 形态下页面与资源全归它)。 */ const fallback = async (_req, res) => {
|
|
1193
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1194
|
+
res.end("client-modules: 没有这条路由\n");
|
|
1195
|
+
};
|
|
1196
|
+
let viteHandoff;
|
|
1197
|
+
let viteStarting;
|
|
1198
|
+
/** 地址只印一次(vite 就绪那一刻,跟它自己的 ready 提示一个意思)。 */
|
|
1199
|
+
let announced = false;
|
|
1200
|
+
const ensureVite = async () => {
|
|
1201
|
+
if (viteHandoff !== void 0) return;
|
|
1202
|
+
viteStarting ??= (async () => {
|
|
1203
|
+
viteHandoff = await startViteDev(ctx, {
|
|
1204
|
+
repoRoot: await resolutionRoot(),
|
|
1205
|
+
base: PLUGIN_ROUTE,
|
|
1206
|
+
rows: { rows: async () => {
|
|
1207
|
+
const patterns = await excludedOf();
|
|
1208
|
+
const all = await rows();
|
|
1209
|
+
return new Map(all.map((row) => [row.id, isAggregated(row.id, patterns) ? `${CLIENT_MODULES_PREFIX}/${encodePackageName(row.id)}@${row.version}/client` : row.clientPath]));
|
|
1210
|
+
} },
|
|
1211
|
+
resolvePluginUrl: (url) => resolvePluginUrl(url),
|
|
1212
|
+
depsUrl: (specifier, importer) => depsUrlOfSpecifier(specifier, importer),
|
|
1213
|
+
httpServer: webServer.server ?? webServer.desktopUpgradeServer
|
|
1214
|
+
});
|
|
1215
|
+
})();
|
|
1216
|
+
await viteStarting;
|
|
1217
|
+
if (!announced) {
|
|
1218
|
+
announced = true;
|
|
1219
|
+
const port = webServer.port;
|
|
1220
|
+
if (port !== void 0) process.stdout.write(`\n ➜ Local: http://127.0.0.1:${String(port)}/\n\n`);
|
|
1221
|
+
}
|
|
1222
|
+
};
|
|
1223
|
+
/** **行**的包名集合(loader 给的 `dsh.client` + client 半):包级引用的出口按它分。 */
|
|
1224
|
+
const clientRowIds = async () => new Set((await rows()).map((row) => row.id));
|
|
1225
|
+
/** 解析根那份 tsconfig 的路径(不存在时返回 undefined,交给 rolldown 默认)。 */
|
|
1226
|
+
const tsconfigOf = async () => {
|
|
1227
|
+
const candidate = (0, node_path.join)(await resolutionRoot(), "tsconfig.json");
|
|
1228
|
+
return await (0, node_fs_promises.readFile)(candidate, "utf8").then(() => true, () => false) ? candidate : void 0;
|
|
1229
|
+
};
|
|
1230
|
+
/**
|
|
1231
|
+
* 对外 URL → **源文件**。`exports` 优先,**没有该键**时退回物理路径(上游多数包没有 `./client` 出口,
|
|
1232
|
+
* 它们的 client 半就是 `src/client/index.ts`)——中间件的改写与插件的 `resolveId` 共用这一份。
|
|
1233
|
+
*/
|
|
1234
|
+
const resolvePluginUrl = async (url) => {
|
|
1235
|
+
const address = parsePluginAddress(url);
|
|
1236
|
+
if (address === void 0) return void 0;
|
|
1237
|
+
const root = await rootOf(address.name);
|
|
1238
|
+
if (root === void 0) return void 0;
|
|
1239
|
+
const manifest = await manifestOf(root);
|
|
1240
|
+
const subpath = address.exportName === "." ? void 0 : address.exportName.slice(2);
|
|
1241
|
+
if (subpath !== void 0) {
|
|
1242
|
+
const byExports = await exportTargetOf(root, subpath);
|
|
1243
|
+
if (byExports !== void 0) return byExports;
|
|
1244
|
+
const pkg = {
|
|
1245
|
+
name: address.name,
|
|
1246
|
+
version: manifest?.version ?? "",
|
|
1247
|
+
root
|
|
1248
|
+
};
|
|
1249
|
+
return resolveInPackage(pkg, (0, node_path.join)("src", subpath, "index.ts")) ?? resolveInPackage(pkg, (0, node_path.join)(subpath, "index.ts")) ?? resolveInPackage(pkg, subpath);
|
|
1250
|
+
}
|
|
1251
|
+
return await exportTargetOf(root, void 0);
|
|
1252
|
+
};
|
|
1253
|
+
const serve = async (req, res) => {
|
|
1254
|
+
if (req.method !== "GET" && req.method !== "HEAD") return fallback(req, res);
|
|
1255
|
+
try {
|
|
1256
|
+
await ensureVite();
|
|
1257
|
+
} catch (error) {
|
|
1258
|
+
ctx.logger.error(error);
|
|
1259
|
+
}
|
|
1260
|
+
if (viteHandoff?.serve(req, res) === true) return;
|
|
1261
|
+
return fallback(req, res);
|
|
1262
|
+
};
|
|
1263
|
+
let cacheRootCache;
|
|
1264
|
+
const cacheRootOf = async () => cacheRootCache ??= await cacheDir();
|
|
1265
|
+
const federationAliasOf = async () => clientModulesAlias({
|
|
1266
|
+
repoRoot: await resolutionRoot(),
|
|
1267
|
+
nodeStub: (0, node_path.join)(appRoot, "node-stub.cjs")
|
|
1268
|
+
});
|
|
1269
|
+
/**
|
|
1270
|
+
* dev 侧源码里的**裸说明符** → 联邦地址;`undefined` 表示"交给 vite 走源码"。
|
|
1271
|
+
*
|
|
1272
|
+
* 判据与产物侧一致:仓库内的**开发模块**(没被 `dsh.dev.exclude` 点名)逐文件走 vite(改它要 HMR);
|
|
1273
|
+
* 其余——三方与"非开发模块"——都由联邦链按包产出(同一条链、同一种地址)。放给 vite 的 `optimizeDeps`
|
|
1274
|
+
* 会多出第二条链:浏览器里同时有 `.vite/deps/react.js` 与我们的 `@client-modules/react@…/index.js`,
|
|
1275
|
+
* 两个 react 实例,hook 分发立刻报 `Invalid hook call`。
|
|
1276
|
+
*/
|
|
1277
|
+
const depsUrlOfSpecifier = async (specifier, importer) => {
|
|
1278
|
+
if (specifier.startsWith("@/")) return void 0;
|
|
1279
|
+
if (_morlay_dsh_cordis_assets_assets.NODE_BUILTIN.test(specifier)) return void 0;
|
|
1280
|
+
const bare = packageNameOf(specifier) || specifier;
|
|
1281
|
+
const root = await rootOf(bare);
|
|
1282
|
+
if (root !== void 0 && !root.includes("node_modules") && !isAggregated(bare, await excludedOf())) return void 0;
|
|
1283
|
+
const base = importer === void 0 || !(0, node_path.isAbsolute)(importer) ? await resolutionManifestPath() : (0, node_url.pathToFileURL)(importer).href;
|
|
1284
|
+
const found = (0, node_module.findPackageJSON)(bare, base);
|
|
1285
|
+
const version = found === void 0 ? void 0 : JSON.parse(await (0, node_fs_promises.readFile)(found, "utf8").catch(() => "{}")).version;
|
|
1286
|
+
return urlOf(specifier, {
|
|
1287
|
+
...version === void 0 ? {} : { versions: /* @__PURE__ */ new Map([[bare, version]]) },
|
|
1288
|
+
clientRows: await clientRowIds()
|
|
1289
|
+
});
|
|
1290
|
+
};
|
|
1291
|
+
/**
|
|
1292
|
+
* `@client-modules/<pkg>@<version>/<file>`:**联邦式**的统一寻址。
|
|
1293
|
+
*
|
|
1294
|
+
* 三方(react)与我们的包在这件事上没有区别——都是"带版本、可独立寻址的一份产物"。落在哪个缓存里
|
|
1295
|
+
* 由"包是不是我们这个工作区的"决定:是 → 按包聚合(整包一次 vite build、产物按出口分开);
|
|
1296
|
+
* 不是 → 三方的预打包。两条链用的是同一份配置。
|
|
1297
|
+
*/
|
|
1298
|
+
const serveFederation = async (url, req, res) => {
|
|
1299
|
+
const cacheRoot = await cacheRootOf();
|
|
1300
|
+
const federationAlias = await federationAliasOf();
|
|
1301
|
+
const rest = url.slice(`/@client-modules/`.length).split("?")[0] ?? "";
|
|
1302
|
+
const slash = rest.indexOf("/");
|
|
1303
|
+
trace("rest %s slash %s", rest, slash);
|
|
1304
|
+
if (slash <= 0) return false;
|
|
1305
|
+
const pkgVersion = rest.slice(0, slash);
|
|
1306
|
+
const file = rest.slice(slash + 1);
|
|
1307
|
+
const at = pkgVersion.lastIndexOf("@");
|
|
1308
|
+
if (at <= 0) return false;
|
|
1309
|
+
const name = decodePackageName(pkgVersion.slice(0, at));
|
|
1310
|
+
const urlVersion = pkgVersion.slice(at + 1);
|
|
1311
|
+
const root = await rootOf(name);
|
|
1312
|
+
const version = (root === void 0 ? void 0 : (await manifestOf(root))?.version) ?? urlVersion;
|
|
1313
|
+
const key = `${name.replaceAll("/", "+")}@${version}`;
|
|
1314
|
+
if (file.startsWith("assets/") || file.startsWith("chunks/")) {
|
|
1315
|
+
const target = JSON.parse(await (0, node_fs_promises.readFile)((0, node_path.join)(cacheRoot, key, ".files.json"), "utf8").catch(() => "{}")).files?.[file];
|
|
1316
|
+
if (target === void 0) return false;
|
|
1317
|
+
const bytes = await (0, node_fs_promises.readFile)((0, node_path.join)(cacheRoot, target)).catch(() => void 0);
|
|
1318
|
+
if (bytes === void 0) return false;
|
|
1319
|
+
const extension = file.slice(file.lastIndexOf(".") + 1).toLowerCase();
|
|
1320
|
+
res.writeHead(200, {
|
|
1321
|
+
"content-type": extension === "js" ? "text/javascript; charset=utf-8" : _morlay_dsh_cordis_assets_assets.ASSET_MIME[extension] ?? "application/octet-stream",
|
|
1322
|
+
"cache-control": "public, max-age=31536000, immutable"
|
|
1323
|
+
});
|
|
1324
|
+
res.end(req.method === "HEAD" ? void 0 : bytes);
|
|
1325
|
+
return true;
|
|
1326
|
+
}
|
|
1327
|
+
const exportName = exportNameOfFileName(file);
|
|
1328
|
+
const entry = root === void 0 ? (await dependencyEntry(await resolutionManifestPath(), name).catch(() => void 0))?.entry : await exportTargetOf(root, exportName === "index" ? void 0 : exportName) ?? await fileInPackage(root, exportName);
|
|
1329
|
+
trace(`[fed] ${name}@${version}/${exportName} root=${String(root)} entry=${String(entry)}\n`);
|
|
1330
|
+
if (entry === void 0) {
|
|
1331
|
+
trace(`[federation] ${name}@${version}/${exportName}: unresolved-entry root=${String(root)}\n`);
|
|
1332
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1333
|
+
res.end(req.method === "HEAD" ? void 0 : `unresolved entry: ${name}@${version}/${exportName}\n`);
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
const bundled = await bundlePackage({
|
|
1337
|
+
cacheRoot,
|
|
1338
|
+
repoRoot: await resolutionRoot(),
|
|
1339
|
+
log: (message) => trace(message),
|
|
1340
|
+
alias: federationAlias,
|
|
1341
|
+
name,
|
|
1342
|
+
version,
|
|
1343
|
+
...root === void 0 ? {} : { manifestOf: (0, node_path.join)(root, "package.json") },
|
|
1344
|
+
clientRows: await clientRowIds(),
|
|
1345
|
+
input: { [exportName]: entry },
|
|
1346
|
+
watch: watchOf(root ?? (await owningPackageOf(entry))?.root, entry),
|
|
1347
|
+
...await tsconfigOf().then((path) => path === void 0 ? {} : { tsconfig: path })
|
|
1348
|
+
}).catch((error) => {
|
|
1349
|
+
ctx.logger.warn(`${name}: bundle-failed ${String(error)}\n`);
|
|
1350
|
+
});
|
|
1351
|
+
if (bundled === void 0) return false;
|
|
1352
|
+
trace(`[fed-tail] ${name}: files=${[...bundled.files.keys()].join(",")} exportName=${exportName}\n`);
|
|
1353
|
+
const target = bundled.files.get(`${exportName}.js`) ?? bundled.files.get(exportName) ?? bundled.files.get("client.js") ?? bundled.files.get("index.js");
|
|
1354
|
+
trace(`${name}: target=${String(target)} 绝对=${String(target === void 0 ? void 0 : (0, node_path.join)(cacheRoot, target))}\n`);
|
|
1355
|
+
if (target === void 0) {
|
|
1356
|
+
ctx.logger.warn(`${name}: missing-export export=${exportName} have=${[...bundled.files.keys()].join(",")}\n`);
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
const code = await (0, node_fs_promises.readFile)((0, node_path.join)(cacheRoot, target), "utf8").catch((error) => {
|
|
1360
|
+
ctx.logger.warn(`${name}: unreadable file=${(0, node_path.join)(cacheRoot, target)} ${String(error)}\n`);
|
|
1361
|
+
});
|
|
1362
|
+
if (code === void 0) return false;
|
|
1363
|
+
const cacheControl = root !== void 0 && !root.includes("node_modules") ? "no-cache" : "public, max-age=31536000, immutable";
|
|
1364
|
+
const etag = `"${bundled.rev}"`;
|
|
1365
|
+
res.writeHead(200, {
|
|
1366
|
+
"content-type": "text/javascript; charset=utf-8",
|
|
1367
|
+
"cache-control": cacheControl,
|
|
1368
|
+
etag
|
|
1369
|
+
});
|
|
1370
|
+
res.end(req.method === "HEAD" ? void 0 : code);
|
|
1371
|
+
return true;
|
|
1372
|
+
};
|
|
1373
|
+
const serveAny = async (req, res) => {
|
|
1374
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//iu.test(req.url ?? "")) {
|
|
1375
|
+
const absolute = new URL(req.url ?? "");
|
|
1376
|
+
req.url = `${absolute.pathname}${absolute.search}`;
|
|
1377
|
+
}
|
|
1378
|
+
const requestPath = (req.url ?? "").split("?")[0] ?? "";
|
|
1379
|
+
const path = requestPath;
|
|
1380
|
+
const known = !/^\/(?:packages|vendor|apps|devpackages|node_modules)(?:\/|$)/u.test(path);
|
|
1381
|
+
if (path.startsWith("/.well-known/")) {
|
|
1382
|
+
res.writeHead(204);
|
|
1383
|
+
res.end();
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
if (!known) {
|
|
1387
|
+
trace("%s referer %s", req.url ?? "", req.headers.referer ?? "-");
|
|
1388
|
+
res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
|
|
1389
|
+
res.end("client-modules: 这是仓库里的路径,打包形态下不存在(页面资源在 app-boot 下、包走 exports)\n");
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
trace(`[serveAny] ${req.url ?? ""}
|
|
1393
|
+
`);
|
|
1394
|
+
if (requestPath === "/" || requestPath === "/index.html") {
|
|
1395
|
+
const html = await (0, node_fs_promises.readFile)((0, node_path.join)(appRoot, "index.html"), "utf8").catch(() => void 0);
|
|
1396
|
+
if (html === void 0) {
|
|
1397
|
+
ctx.logger.warn(`[serveAny] 页面骨架读不到:${(0, node_path.join)(appRoot, "index.html")}\n`);
|
|
1398
|
+
return fallback(req, res);
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
await ensureVite();
|
|
1402
|
+
} catch (error) {
|
|
1403
|
+
ctx.logger.error(error);
|
|
1404
|
+
}
|
|
1405
|
+
const injected = viteHandoff?.hmr === true ? html.replace("</head>", ` <script type="module" src="/@vite/client"><\/script>\n </head>`) : html;
|
|
1406
|
+
res.writeHead(200, {
|
|
1407
|
+
"content-type": "text/html; charset=utf-8",
|
|
1408
|
+
"cache-control": "no-cache"
|
|
1409
|
+
});
|
|
1410
|
+
res.end(req.method === "HEAD" ? void 0 : injected);
|
|
1411
|
+
return;
|
|
1412
|
+
}
|
|
1413
|
+
const requestUrl = req.url ?? "";
|
|
1414
|
+
if (requestUrl.split("?")[0]?.startsWith(`/@client-modules/`) === true) {
|
|
1415
|
+
const handled = await serveFederation(requestUrl, req, res);
|
|
1416
|
+
trace("%s handled %s status %s", requestUrl, handled, res.statusCode);
|
|
1417
|
+
if (handled) return;
|
|
1418
|
+
}
|
|
1419
|
+
try {
|
|
1420
|
+
await ensureVite();
|
|
1421
|
+
} catch (error) {
|
|
1422
|
+
ctx.logger.error(error);
|
|
1423
|
+
}
|
|
1424
|
+
if (viteHandoff !== void 0 && viteHandoff.serve(req, res)) {
|
|
1425
|
+
trace("%s status %s", path, res.statusCode);
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
if (path.startsWith("/@")) ctx.logger.warn(`${req.url ?? ""} referer=${req.headers.referer ?? "-"}\n`);
|
|
1429
|
+
return fallback(req, res);
|
|
1430
|
+
};
|
|
1431
|
+
ctx.effect(() => webServer.registerFallback(serveAny), "client-modules: vite 兜底");
|
|
1432
|
+
ctx.effect(() => webServer.registerUpgrade({
|
|
1433
|
+
path: HMR_PATH,
|
|
1434
|
+
handler: () => {}
|
|
1435
|
+
}), "client-modules: vite hmr upgrade");
|
|
1436
|
+
ctx.effect(() => webServer.register({
|
|
1437
|
+
kind: "prefix",
|
|
1438
|
+
path: PLUGIN_ROUTE,
|
|
1439
|
+
handler: serve
|
|
1440
|
+
}), "client-modules: /plugins route");
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
exports.apply = apply;
|
|
1444
|
+
exports.inject = inject;
|
|
1445
|
+
exports.name = name;
|