@morlay/dsh-client-modules 0.2.11-alpha.3 → 0.2.11

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.
@@ -0,0 +1,128 @@
1
+ /**
2
+ * 浏览器构建里烘焙的版本元数据。
3
+ *
4
+ * 上游的 client 构建(`scripts/client-build-environment.ts`)把一组 `DSH_CLIENT_*` 值当 `define` 注入产物:
5
+ * - `DSH_CLIENT_VERSION`:设置页「当前版本」那一行;
6
+ * - `DSH_CLIENT_COMMIT_HASH` + `DSH_CLIENT_GIT_DIRTY`:侧栏显示的 `<commit>` / `<commit>-dirty`;
7
+ * - `DSH_CLIENT_BUILD_PROFILE`:`official` / `local`(官方品牌只在 official 下出现)。
8
+ *
9
+ * 这条链以前把 `process.env` 整个替换成 `{ env: {} }`,于是这些值全是 `undefined`:版本行不显示、
10
+ * `ui-settings-account` 还会因为 `DSH_CLIENT_VERSION` 缺失直接抛错。取值顺序(越靠前越权威):
11
+ *
12
+ * 1. 宿主进程的 `DSH_CLIENT_*`(`dshdev` 与桌面壳在起宿主时注入);
13
+ * 2. 仓库根的 `.dsh-build/client-build-environment.json`(`just prepare` 里的 `dshdev client-meta` 写);
14
+ * 3. 现算:仓库根的 `package.json` 版本 + `git rev-parse` / `git status`(dev 常见,代价是几次 git 调用)。
15
+ */
16
+ import { execFile } from "node:child_process";
17
+ import { readFile } from "node:fs/promises";
18
+ import { join } from "node:path";
19
+ import { promisify } from "node:util";
20
+
21
+ /** 一份构建元数据;缺项表示该值不可得(前端自己会判 `undefined`)。 */
22
+ export interface ClientBuildMeta {
23
+ readonly version?: string;
24
+ readonly commit?: string;
25
+ readonly dirty?: boolean;
26
+ /** `DSH_CLIENT_BUILD_PROFILE`:`official` / `local`,由上游的构建环境定义,缺了就不注入。 */
27
+ readonly profile?: string;
28
+ }
29
+
30
+ /** `prepare` 写、构建读的那份记录(形状与上游 `client-build-environment.json` 一致)。 */
31
+ export const CLIENT_BUILD_RECORD = join(".dsh-build", "client-build-environment.json");
32
+
33
+ const VERSION = "DSH_CLIENT_VERSION";
34
+ const COMMIT = "DSH_CLIENT_COMMIT_HASH";
35
+ const DIRTY = "DSH_CLIENT_GIT_DIRTY";
36
+ const PROFILE = "DSH_CLIENT_BUILD_PROFILE";
37
+
38
+ /** 环境里那组 `DSH_CLIENT_*` → 元数据。 */
39
+ function metaOfEnvironment(environment: NodeJS.ProcessEnv): ClientBuildMeta {
40
+ const version = environment[VERSION];
41
+ const commit = environment[COMMIT];
42
+ const dirty = environment[DIRTY];
43
+ const profile = environment[PROFILE];
44
+ return {
45
+ ...(version === undefined || version === "" ? {} : { version }),
46
+ ...(commit === undefined || commit === "" ? {} : { commit }),
47
+ ...(dirty === "true" ? { dirty: true } : {}),
48
+ ...(profile === undefined || profile === "" ? {} : { profile }),
49
+ };
50
+ }
51
+
52
+ const runFile = promisify(execFile);
53
+
54
+ /** `git` 的输出;不在 git 工作树里、或 git 不可用时 undefined。 */
55
+ async function gitOutput(root: string, args: readonly string[]): Promise<string | undefined> {
56
+ try {
57
+ const { stdout } = await runFile("git", args, { cwd: root, encoding: "utf8" });
58
+ return stdout.trim();
59
+ } catch {
60
+ return undefined;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * 现算:**装的是哪一版 harness**(`DEEPSEEK_HARNESS_VERSION`,与同步链同一个真源)+ 本仓库的 git 元数据。
66
+ *
67
+ * 正常路径拿不到这里:`dshdev` 会注入环境、`prepare` 会写记录。只有"绕过 dshdev 直接构建 client 产物"
68
+ * 时才走到,所以版本只能取环境或仓库根的 `package.json`(后者不是权威值,缺 env 时用它兜底)。
69
+ */
70
+ async function computeMeta(repoRoot: string): Promise<ClientBuildMeta> {
71
+ const explicit = process.env["DEEPSEEK_HARNESS_VERSION"];
72
+ let version: string | undefined = explicit === undefined || explicit === "" ? undefined : explicit;
73
+ if (version === undefined) {
74
+ const manifest = await readFile(join(repoRoot, "package.json"), "utf8").catch(() => undefined);
75
+ if (manifest !== undefined) {
76
+ const parsed = JSON.parse(manifest) as { version?: unknown };
77
+ if (typeof parsed.version === "string" && parsed.version !== "") version = parsed.version;
78
+ }
79
+ }
80
+ const commit = await gitOutput(repoRoot, ["rev-parse", "--short=7", "HEAD"]);
81
+ const status = await gitOutput(repoRoot, ["status", "--porcelain", "--untracked-files=normal"]);
82
+ return {
83
+ ...(version === undefined ? {} : { version }),
84
+ ...(commit === undefined ? {} : { commit }),
85
+ ...(status === undefined || status === "" ? {} : { dirty: true }),
86
+ };
87
+ }
88
+
89
+ /** 读 `prepare` 写下的那份记录(缺失 / 坏掉都给 undefined,不挡构建)。 */
90
+ async function recordedMeta(repoRoot: string): Promise<ClientBuildMeta | undefined> {
91
+ const raw = await readFile(join(repoRoot, CLIENT_BUILD_RECORD), "utf8").catch(() => undefined);
92
+ if (raw === undefined) return undefined;
93
+ try {
94
+ const parsed = JSON.parse(raw) as { environment?: Record<string, string> };
95
+ const environment = parsed.environment ?? {};
96
+ return metaOfEnvironment(environment as NodeJS.ProcessEnv);
97
+ } catch {
98
+ return undefined;
99
+ }
100
+ }
101
+
102
+ /**
103
+ * 这个仓库在浏览器产物里该带的版本元数据。
104
+ * @param repoRoot - 仓库根(dev 是工作区,打包形态是 profile:那里通常读不到记录,靠宿主注入的环境)。
105
+ */
106
+ export async function clientBuildMeta(repoRoot: string): Promise<ClientBuildMeta> {
107
+ const inherited = metaOfEnvironment(process.env);
108
+ if (inherited.version !== undefined && inherited.commit !== undefined) return inherited;
109
+ const recorded = await recordedMeta(repoRoot);
110
+ const computed = recorded ?? (await computeMeta(repoRoot));
111
+ return { ...computed, ...inherited };
112
+ }
113
+
114
+ /**
115
+ * `define` 用的一组键:`process.env.DSH_CLIENT_*` → 字面量。
116
+ *
117
+ * `browserProcessPlugin` 会跳过这些键(见那里的负向前瞻),所以 define 这一层能把它们换成真实值;
118
+ * 其余 `process.env` 仍是空壳对象。
119
+ */
120
+ export function clientMetaDefines(meta: ClientBuildMeta): Record<string, string> {
121
+ return {
122
+ [`process.env.${VERSION}`]: meta.version === undefined ? "undefined" : JSON.stringify(meta.version),
123
+ [`process.env.${COMMIT}`]: meta.commit === undefined ? "undefined" : JSON.stringify(meta.commit),
124
+ [`process.env.${DIRTY}`]: meta.dirty === true ? JSON.stringify("true") : "undefined",
125
+ // 官方品牌(`ui-brand-official`)按这个值判断:值也来自上游的构建环境,缺了就不注入。
126
+ [`process.env.${PROFILE}`]: meta.profile === undefined ? "undefined" : JSON.stringify(meta.profile),
127
+ };
128
+ }
package/src/index.ts CHANGED
@@ -398,6 +398,10 @@ export function apply(ctx: Context): void {
398
398
  ): Promise<string | undefined> => {
399
399
  // `@/…` 是本地文件(alias 管),不是包——`packageNameOf` 会把它切成 `@/vendor` 这种假包名。
400
400
  if (specifier.startsWith("@/")) return undefined;
401
+ // 样式引用交给 vite 的 CSS 管线内联:改写成联邦 URL 会让浏览器把 `text/css` 当 ESM 模块加载
402
+ // (`Failed to load module script: … MIME type "text/css"`)。联邦构建那侧有对应的内联(见
403
+ // `dsh-css-specifiers`)。
404
+ if (specifier.endsWith(".css")) return undefined;
401
405
  // `zlib` / `stream` 这类**裸名的 node 内置**不是包:接管成联邦地址只会换来一串 404。留给 alias
402
406
  // 的替身(名单同一份)。
403
407
  if (NODE_BUILTIN.test(specifier)) return undefined;
@@ -579,8 +583,11 @@ export function apply(ctx: Context): void {
579
583
  // **不发 304**:这边的响应最终要由上游包成 Fetch `Response`,而它不接受 304
580
584
  // (`Invalid response status code 304`)——请求会以异常告终。产物本来就不大,直接 200 全量。
581
585
  const etag = `"${bundled.rev}"`;
586
+ // 产物类型由文件扩展名决定:CSS 出口就是 CSS 文本,其余是 JS 模块。
587
+ const extension = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
582
588
  res.writeHead(200, {
583
- "content-type": "text/javascript; charset=utf-8",
589
+ "content-type":
590
+ extension === "css" ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8",
584
591
  "cache-control": cacheControl,
585
592
  etag,
586
593
  });
package/src/resolve.ts CHANGED
@@ -268,7 +268,8 @@ export async function exportTargetOf(
268
268
  // `exports` 的三种形状(都要认):
269
269
  // · 以 `.` 开头的键映射(标准)→ 取 `exports["."]` / `exports["./sub"]`;
270
270
  // · **条件对象本身**(`{ development, default }`,`mdast-util-*` 就是这形状)→ 它**就是**根出口;
271
- // · 没有 `exports` → 按 node 规则退回 `module` → `main`(老包如 `fortune-sheet`)。
271
+ // · 没有 `exports` → 打包器约定:`module`(ESM)→ `main`。有 ESM 就走 ESM,不把双形态包
272
+ // 降级成 CJS 再来适配(`rrule` / `@xterm/xterm` 的 `module` 都是它们的 ESM 面)。
272
273
  const table = manifest.exports;
273
274
  const isSubpathMap =
274
275
  table !== undefined &&
package/src/serve-vite.ts CHANGED
@@ -11,7 +11,8 @@ import { fileURLToPath } from "node:url";
11
11
  import type { Context } from "@deepseek-ai/cordis";
12
12
  import { traceOf } from "./trace.ts";
13
13
  import { createServer, type ServerOptions, type ViteDevServer } from "vite";
14
- import { clientModulesAlias } from "./bundle.ts";
14
+ import { browserProcessPlugin, clientModulesAlias } from "./bundle.ts";
15
+ import { clientBuildMeta } from "./client-meta.ts";
15
16
  import { dshClientRowsPlugin, ROWS_VIRTUAL } from "./vite-plugin.ts";
16
17
  import { packageRootOf } from "./resolve.ts";
17
18
 
@@ -93,11 +94,9 @@ export async function startViteDev(
93
94
  // **不要**把 base 改成 `/@/` 去"统一前缀":vite 会给所有不以 base 开头的绝对 URL 再补一层,
94
95
  // 联邦地址(`/@client-modules/…`)会变成 `/@/@client-modules/…`,两条链全断。
95
96
  base: "/",
96
- // 浏览器没有 `process`:客户端半常读 `process.env.NODE_ENV`(上游 client 打包也做同样的替换)。
97
+ // 浏览器没有 `process`:裸 `process` 给空壳(逐文件服务的源码里会读它)。`process.env` 与版本元数据
98
+ // (`DSH_CLIENT_*`)由 `browserProcessPlugin` 处理——与联邦构建同一份规则,顺序只写在一处。
97
99
  define: {
98
- "process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] ?? "development"),
99
- "process.env": "{ env: {} }",
100
- // 裸 `process` 同样要给空壳(逐文件服务的源码里也会读它)。
101
100
  process: "{ env: {} }",
102
101
  },
103
102
  publicDir: false,
@@ -151,6 +150,8 @@ export async function startViteDev(
151
150
  // 官方口径的"整个关掉"就是这两个字段(`optimizeDeps.disabled` 在 Vite 5.1 已移除,传它只会换来一条告警)。
152
151
  optimizeDeps: { noDiscovery: true, include: [] },
153
152
  plugins: [
153
+ // 与联邦构建同一份 `process.env` 处理(含 `DSH_CLIENT_*` 的版本元数据)。
154
+ browserProcessPlugin(await clientBuildMeta(repoRoot)),
154
155
  dshClientRowsPlugin({
155
156
  repoRoot,
156
157
  rows: () => rowsOf(options.rows),