@morlay/dsh-client-modules 0.2.11-alpha.3 → 0.2.12
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/client.cjs +2483 -1481
- package/dist/index.cjs +359 -115
- package/dist/index.mjs +361 -117
- package/package.json +8 -8
- package/src/__tests__/bundle.spec.ts +305 -60
- package/src/__tests__/client-meta.spec.ts +114 -0
- package/src/__tests__/resolve.spec.ts +20 -2
- package/src/bundle.ts +303 -141
- package/src/client-meta.ts +128 -0
- package/src/index.ts +8 -1
- package/src/resolve.ts +2 -1
- package/src/serve-vite.ts +6 -5
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createRequire, findPackageJSON } from "node:module";
|
|
2
|
-
import { appendFile, mkdir, readFile, readdir,
|
|
3
|
-
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
5
|
import { ASSET_MIME, NODE_BUILTIN, clientAssetPlugin } from "@morlay/dsh-cordis-assets/assets";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { cssInlinePlugins } from "@morlay/dsh-cordis-assets/styles";
|
|
8
8
|
import { rolldown } from "rolldown";
|
|
9
9
|
import { createServer, esmExternalRequirePlugin } from "vite";
|
|
10
|
+
import { execFile } from "node:child_process";
|
|
11
|
+
import { promisify } from "node:util";
|
|
10
12
|
// src/trace.ts
|
|
11
13
|
/**
|
|
12
14
|
* 诊断细节(逐包预编译、逐请求那一类)的具名 logger。
|
|
@@ -85,6 +87,122 @@ async function devExcluded(profileDir) {
|
|
|
85
87
|
function isAggregated(name, patterns) {
|
|
86
88
|
return patterns.some((pattern) => pattern.test(name));
|
|
87
89
|
}
|
|
90
|
+
// src/client-meta.ts
|
|
91
|
+
/**
|
|
92
|
+
* 浏览器构建里烘焙的版本元数据。
|
|
93
|
+
*
|
|
94
|
+
* 上游的 client 构建(`scripts/client-build-environment.ts`)把一组 `DSH_CLIENT_*` 值当 `define` 注入产物:
|
|
95
|
+
* - `DSH_CLIENT_VERSION`:设置页「当前版本」那一行;
|
|
96
|
+
* - `DSH_CLIENT_COMMIT_HASH` + `DSH_CLIENT_GIT_DIRTY`:侧栏显示的 `<commit>` / `<commit>-dirty`;
|
|
97
|
+
* - `DSH_CLIENT_BUILD_PROFILE`:`official` / `local`(官方品牌只在 official 下出现)。
|
|
98
|
+
*
|
|
99
|
+
* 这条链以前把 `process.env` 整个替换成 `{ env: {} }`,于是这些值全是 `undefined`:版本行不显示、
|
|
100
|
+
* `ui-settings-account` 还会因为 `DSH_CLIENT_VERSION` 缺失直接抛错。取值顺序(越靠前越权威):
|
|
101
|
+
*
|
|
102
|
+
* 1. 宿主进程的 `DSH_CLIENT_*`(`dshdev` 与桌面壳在起宿主时注入);
|
|
103
|
+
* 2. 仓库根的 `.dsh-build/client-build-environment.json`(`just prepare` 里的 `dshdev client-meta` 写);
|
|
104
|
+
* 3. 现算:仓库根的 `package.json` 版本 + `git rev-parse` / `git status`(dev 常见,代价是几次 git 调用)。
|
|
105
|
+
*/
|
|
106
|
+
/** `prepare` 写、构建读的那份记录(形状与上游 `client-build-environment.json` 一致)。 */
|
|
107
|
+
const CLIENT_BUILD_RECORD = join(".dsh-build", "client-build-environment.json");
|
|
108
|
+
const VERSION = "DSH_CLIENT_VERSION";
|
|
109
|
+
const COMMIT = "DSH_CLIENT_COMMIT_HASH";
|
|
110
|
+
const DIRTY = "DSH_CLIENT_GIT_DIRTY";
|
|
111
|
+
const PROFILE = "DSH_CLIENT_BUILD_PROFILE";
|
|
112
|
+
/** 环境里那组 `DSH_CLIENT_*` → 元数据。 */
|
|
113
|
+
function metaOfEnvironment(environment) {
|
|
114
|
+
const version = environment[VERSION];
|
|
115
|
+
const commit = environment[COMMIT];
|
|
116
|
+
const dirty = environment[DIRTY];
|
|
117
|
+
const profile = environment[PROFILE];
|
|
118
|
+
return {
|
|
119
|
+
...version === void 0 || version === "" ? {} : { version },
|
|
120
|
+
...commit === void 0 || commit === "" ? {} : { commit },
|
|
121
|
+
...dirty === "true" ? { dirty: true } : {},
|
|
122
|
+
...profile === void 0 || profile === "" ? {} : { profile }
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const runFile = promisify(execFile);
|
|
126
|
+
/** `git` 的输出;不在 git 工作树里、或 git 不可用时 undefined。 */
|
|
127
|
+
async function gitOutput(root, args) {
|
|
128
|
+
try {
|
|
129
|
+
const { stdout } = await runFile("git", args, {
|
|
130
|
+
cwd: root,
|
|
131
|
+
encoding: "utf8"
|
|
132
|
+
});
|
|
133
|
+
return stdout.trim();
|
|
134
|
+
} catch {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* 现算:**装的是哪一版 harness**(`DEEPSEEK_HARNESS_VERSION`,与同步链同一个真源)+ 本仓库的 git 元数据。
|
|
140
|
+
*
|
|
141
|
+
* 正常路径拿不到这里:`dshdev` 会注入环境、`prepare` 会写记录。只有"绕过 dshdev 直接构建 client 产物"
|
|
142
|
+
* 时才走到,所以版本只能取环境或仓库根的 `package.json`(后者不是权威值,缺 env 时用它兜底)。
|
|
143
|
+
*/
|
|
144
|
+
async function computeMeta(repoRoot) {
|
|
145
|
+
const explicit = process.env["DEEPSEEK_HARNESS_VERSION"];
|
|
146
|
+
let version = explicit === void 0 || explicit === "" ? void 0 : explicit;
|
|
147
|
+
if (version === void 0) {
|
|
148
|
+
const manifest = await readFile(join(repoRoot, "package.json"), "utf8").catch(() => void 0);
|
|
149
|
+
if (manifest !== void 0) {
|
|
150
|
+
const parsed = JSON.parse(manifest);
|
|
151
|
+
if (typeof parsed.version === "string" && parsed.version !== "") version = parsed.version;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const commit = await gitOutput(repoRoot, [
|
|
155
|
+
"rev-parse",
|
|
156
|
+
"--short=7",
|
|
157
|
+
"HEAD"
|
|
158
|
+
]);
|
|
159
|
+
const status = await gitOutput(repoRoot, [
|
|
160
|
+
"status",
|
|
161
|
+
"--porcelain",
|
|
162
|
+
"--untracked-files=normal"
|
|
163
|
+
]);
|
|
164
|
+
return {
|
|
165
|
+
...version === void 0 ? {} : { version },
|
|
166
|
+
...commit === void 0 ? {} : { commit },
|
|
167
|
+
...status === void 0 || status === "" ? {} : { dirty: true }
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/** 读 `prepare` 写下的那份记录(缺失 / 坏掉都给 undefined,不挡构建)。 */
|
|
171
|
+
async function recordedMeta(repoRoot) {
|
|
172
|
+
const raw = await readFile(join(repoRoot, CLIENT_BUILD_RECORD), "utf8").catch(() => void 0);
|
|
173
|
+
if (raw === void 0) return void 0;
|
|
174
|
+
try {
|
|
175
|
+
return metaOfEnvironment(JSON.parse(raw).environment ?? {});
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* 这个仓库在浏览器产物里该带的版本元数据。
|
|
182
|
+
* @param repoRoot - 仓库根(dev 是工作区,打包形态是 profile:那里通常读不到记录,靠宿主注入的环境)。
|
|
183
|
+
*/
|
|
184
|
+
async function clientBuildMeta(repoRoot) {
|
|
185
|
+
const inherited = metaOfEnvironment(process.env);
|
|
186
|
+
if (inherited.version !== void 0 && inherited.commit !== void 0) return inherited;
|
|
187
|
+
return {
|
|
188
|
+
...await recordedMeta(repoRoot) ?? await computeMeta(repoRoot),
|
|
189
|
+
...inherited
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* `define` 用的一组键:`process.env.DSH_CLIENT_*` → 字面量。
|
|
194
|
+
*
|
|
195
|
+
* `browserProcessPlugin` 会跳过这些键(见那里的负向前瞻),所以 define 这一层能把它们换成真实值;
|
|
196
|
+
* 其余 `process.env` 仍是空壳对象。
|
|
197
|
+
*/
|
|
198
|
+
function clientMetaDefines(meta) {
|
|
199
|
+
return {
|
|
200
|
+
[`process.env.${VERSION}`]: meta.version === void 0 ? "undefined" : JSON.stringify(meta.version),
|
|
201
|
+
[`process.env.${COMMIT}`]: meta.commit === void 0 ? "undefined" : JSON.stringify(meta.commit),
|
|
202
|
+
[`process.env.${DIRTY}`]: meta.dirty === true ? JSON.stringify("true") : "undefined",
|
|
203
|
+
[`process.env.${PROFILE}`]: meta.profile === void 0 ? "undefined" : JSON.stringify(meta.profile)
|
|
204
|
+
};
|
|
205
|
+
}
|
|
88
206
|
// src/resolve.ts
|
|
89
207
|
/** 路由前缀(沿用上游的 `/plugins`,页面上不改 URL 形态)。 */
|
|
90
208
|
const PLUGIN_ROUTE = "/plugins";
|
|
@@ -385,7 +503,7 @@ function clientModulesAlias(options) {
|
|
|
385
503
|
* 实例)。内容变了**不换 URL**,靠 `cache-control: no-cache` + `ETag` 协商(vite dev 对它自己的模块
|
|
386
504
|
* 也是这套)。
|
|
387
505
|
*/
|
|
388
|
-
const BUNDLER_REV = createHash("sha1").update(`client-modules:rules:
|
|
506
|
+
const BUNDLER_REV = createHash("sha1").update(`client-modules:rules:36`).digest("hex").slice(0, 8);
|
|
389
507
|
/** 包 → 它在浏览器里的地址(联邦式)。 */
|
|
390
508
|
function depsUrlOf(specifier, version, file = "index.js") {
|
|
391
509
|
const name = packageNameOf(specifier) || specifier;
|
|
@@ -512,8 +630,13 @@ function urlOf(specifier, tables) {
|
|
|
512
630
|
* **只替换 `process.env` 系列**:裸的 `process` 不能碰——它可能只是个普通标识符(`zod` 里就有一个叫
|
|
513
631
|
* `process` 的函数,替换掉直接把源码变成语法错误)。
|
|
514
632
|
*/
|
|
515
|
-
function browserProcessPlugin() {
|
|
516
|
-
const
|
|
633
|
+
function browserProcessPlugin(meta = {}) {
|
|
634
|
+
const metaDefines = Object.entries(clientMetaDefines(meta));
|
|
635
|
+
const replace = (code) => {
|
|
636
|
+
let next = code.replaceAll(/\bprocess\.env\.NODE_ENV\b/gu, JSON.stringify("development"));
|
|
637
|
+
for (const [key, value] of metaDefines) next = next.replaceAll(key, value);
|
|
638
|
+
return next.replaceAll(/\bprocess\.env\b/gu, "({ env: {} })");
|
|
639
|
+
};
|
|
517
640
|
return {
|
|
518
641
|
name: "dsh-browser-process",
|
|
519
642
|
enforce: "pre",
|
|
@@ -527,118 +650,205 @@ function browserProcessPlugin() {
|
|
|
527
650
|
};
|
|
528
651
|
}
|
|
529
652
|
/**
|
|
530
|
-
*
|
|
653
|
+
* `export const <名>` 用不了的键:JS 的保留字(`zod@4` 的 CJS 面有 `function` / `void` / `null`
|
|
654
|
+
* 这些键,原样拼出去就是 `SyntaxError`)。
|
|
655
|
+
*/
|
|
656
|
+
const RESERVED_EXPORT_NAMES = /* @__PURE__ */ new Set([
|
|
657
|
+
"await",
|
|
658
|
+
"break",
|
|
659
|
+
"case",
|
|
660
|
+
"catch",
|
|
661
|
+
"class",
|
|
662
|
+
"const",
|
|
663
|
+
"continue",
|
|
664
|
+
"debugger",
|
|
665
|
+
"default",
|
|
666
|
+
"delete",
|
|
667
|
+
"do",
|
|
668
|
+
"else",
|
|
669
|
+
"enum",
|
|
670
|
+
"eval",
|
|
671
|
+
"export",
|
|
672
|
+
"extends",
|
|
673
|
+
"false",
|
|
674
|
+
"finally",
|
|
675
|
+
"for",
|
|
676
|
+
"function",
|
|
677
|
+
"if",
|
|
678
|
+
"implements",
|
|
679
|
+
"import",
|
|
680
|
+
"in",
|
|
681
|
+
"instanceof",
|
|
682
|
+
"interface",
|
|
683
|
+
"let",
|
|
684
|
+
"new",
|
|
685
|
+
"null",
|
|
686
|
+
"package",
|
|
687
|
+
"private",
|
|
688
|
+
"protected",
|
|
689
|
+
"public",
|
|
690
|
+
"return",
|
|
691
|
+
"static",
|
|
692
|
+
"super",
|
|
693
|
+
"switch",
|
|
694
|
+
"this",
|
|
695
|
+
"throw",
|
|
696
|
+
"true",
|
|
697
|
+
"try",
|
|
698
|
+
"typeof",
|
|
699
|
+
"undefined",
|
|
700
|
+
"var",
|
|
701
|
+
"void",
|
|
702
|
+
"while",
|
|
703
|
+
"with",
|
|
704
|
+
"yield"
|
|
705
|
+
]);
|
|
706
|
+
/** ESM 的顶层语法节点:`import` / `export` 声明。 */
|
|
707
|
+
const ESM_STATEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
708
|
+
"ImportDeclaration",
|
|
709
|
+
"ExportNamedDeclaration",
|
|
710
|
+
"ExportDefaultDeclaration",
|
|
711
|
+
"ExportAllDeclaration"
|
|
712
|
+
]);
|
|
713
|
+
/**
|
|
714
|
+
* `.js` 且没有 `type` 时,按 **Node 的语法检测**判:有 ESM 语法(顶层 `import`/`export`)就是 ESM,
|
|
715
|
+
* 否则 CJS(Node 23+ 默认开启 `--experimental-detect-module`)。
|
|
716
|
+
*
|
|
717
|
+
* 用 rolldown 自带的解析器(`rolldown/parseAst`),不自己写词法扫描。
|
|
718
|
+
*
|
|
719
|
+
* @param entry - `.js` 入口的绝对路径。
|
|
720
|
+
* @returns 是否含 ESM 语法。
|
|
721
|
+
*/
|
|
722
|
+
async function hasModuleSyntax(entry) {
|
|
723
|
+
try {
|
|
724
|
+
const [{ parseAst }, source] = await Promise.all([import("rolldown/parseAst"), readFile(entry, "utf8")]);
|
|
725
|
+
return parseAst(source, null, entry).body.some((node) => ESM_STATEMENT_TYPES.has(node.type));
|
|
726
|
+
} catch {
|
|
727
|
+
return false;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* 一个入口的模块类型:**扩展名 + 最近的 `package.json`**,不猜源码。
|
|
531
732
|
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
733
|
+
* | 入口 | 类型 |
|
|
734
|
+
* | --- | --- |
|
|
735
|
+
* | `.mjs` / `.mts` | `esm` |
|
|
736
|
+
* | `.cjs` / `.cts` | `cjs` |
|
|
737
|
+
* | `.js` + `type: "module"` | `esm` |
|
|
738
|
+
* | `.js` + `module` 字段指向它 | `esm`(打包器约定) |
|
|
739
|
+
* | 其余 `.js` | `cjs` |
|
|
740
|
+
* | `.ts` / `.tsx` / `.jsx` | `source`(转译面,原样) |
|
|
741
|
+
* | `.css` | `style` |
|
|
742
|
+
* | 其他(图片 / 字体 / `.json` …) | `asset` |
|
|
536
743
|
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
744
|
+
* 这是 `exports` 条件(`import` / `require`)选出入口之后的**第二步**:条件决定"哪个文件",这里决定
|
|
745
|
+
* "那个文件是什么模块"。只有 `cjs` 需要导出面适配,`esm` / `source` 一律原样。
|
|
746
|
+
*
|
|
747
|
+
* @param entry - 入口的绝对路径。
|
|
748
|
+
* @returns 模块类型。
|
|
539
749
|
*/
|
|
540
|
-
async function
|
|
541
|
-
|
|
542
|
-
if (
|
|
543
|
-
if (
|
|
750
|
+
async function moduleKindOf(entry) {
|
|
751
|
+
const extension = extname(entry).toLowerCase();
|
|
752
|
+
if (extension === ".css") return "style";
|
|
753
|
+
if (extension === ".mjs" || extension === ".mts") return "esm";
|
|
754
|
+
if (extension === ".cjs" || extension === ".cts") return "cjs";
|
|
755
|
+
if (extension === ".ts" || extension === ".tsx" || extension === ".jsx") return "source";
|
|
756
|
+
if (extension !== ".js") return "asset";
|
|
544
757
|
const manifestPath = await packageJsonNear(entry);
|
|
545
758
|
const manifest = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{}"));
|
|
546
|
-
if (manifest.type === "module") return
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
return resolve(dirname(manifestPath), manifestModule) !== resolve(entry);
|
|
759
|
+
if (manifest.type === "module") return "esm";
|
|
760
|
+
if (manifest.type === "commonjs") return "cjs";
|
|
761
|
+
return await hasModuleSyntax(entry) ? "esm" : "cjs";
|
|
550
762
|
}
|
|
551
763
|
/**
|
|
552
|
-
* CJS 入口的**具名导出包装**(`react` 这类)。
|
|
764
|
+
* CJS 入口的**具名导出包装**(`react` / `@xterm/xterm` 这类)。
|
|
765
|
+
*
|
|
766
|
+
* rolldown 对 CJS 入口的产物的具名导出只能靠静态分析;`module.exports = require(…)` 与 UMD 包装它都分析
|
|
767
|
+
* 不出来,产物只剩 `export default …`——而浏览器侧要 `import { Fragment } from "react"`、`import { Terminal }
|
|
768
|
+
* from "@xterm/xterm"`,整个 import 阶段就失败。ESM 的导出必须**静态可枚举**,所以这里求值入口拿到它的键,
|
|
769
|
+
* 生成一份把它们写成 `export const` 的包装文件,拿包装文件当入口。
|
|
553
770
|
*
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
* 阶段就失败**(入口一行都执行不了)。ESM 的导出必须**静态可枚举**(不能 `export * from` 一个运行期对象),
|
|
557
|
-
* 所以这里求值入口拿到它的键,生成一份把它们写成 `export const` 的包装文件,拿包装文件当入口。
|
|
771
|
+
* 类型由 {@link moduleKindOf} 先按 **exports 落点 + 扩展名/type** 静态判定,只有 CJS 才进来;
|
|
772
|
+
* 这里求值一次只为**拿键**(`export const <键>` + `default = module.exports`)。ESM 原封不动。
|
|
558
773
|
*
|
|
559
|
-
* @returns 包装文件路径;入口不是 CJS
|
|
774
|
+
* @returns 包装文件路径;入口不是 node 能求值的 CJS、或没有可静态导出的键时 undefined(保持原样)。
|
|
560
775
|
*/
|
|
561
776
|
async function cjsShimOf(options) {
|
|
562
|
-
|
|
563
|
-
let keys;
|
|
777
|
+
let loaded;
|
|
564
778
|
try {
|
|
565
|
-
|
|
566
|
-
keys = Object.keys(loaded ?? {});
|
|
779
|
+
loaded = createRequire(options.entry)(options.entry);
|
|
567
780
|
} catch {
|
|
568
781
|
return;
|
|
569
782
|
}
|
|
570
|
-
const named = keys.filter((key) => key !== "default" && /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key));
|
|
783
|
+
const named = Object.keys(loaded ?? {}).filter((key) => key !== "default" && /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(key) && !RESERVED_EXPORT_NAMES.has(key));
|
|
571
784
|
if (named.length === 0) return void 0;
|
|
572
|
-
const
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
`import mod from ${JSON.stringify(options.entry)};`,
|
|
785
|
+
const id = `\0dsh-cjs-shim:${options.name}:${options.exportName}`;
|
|
786
|
+
const code = [
|
|
787
|
+
`import mod from ${JSON.stringify(options.from)};`,
|
|
576
788
|
"const __cjs = mod ?? {};",
|
|
577
789
|
"export default __cjs;",
|
|
578
790
|
...named.map((key) => `export const ${key} = __cjs[${JSON.stringify(key)}];`),
|
|
579
791
|
""
|
|
580
|
-
].join("\n")
|
|
792
|
+
].join("\n");
|
|
581
793
|
options.log?.(`[bundle-cjs] ${options.name}/${options.exportName}: 补 ${String(named.length)} 个具名导出\n`);
|
|
582
|
-
return file;
|
|
583
|
-
}
|
|
584
|
-
/** 根出口的产物名(`urlOf` 对非 client 行写的就是它)。 */
|
|
585
|
-
const ROOT_EXPORT = "index";
|
|
586
|
-
/** 具名导出的合法标识符:补 default 时按它挑键(非标识符的导出名用不了简写)。 */
|
|
587
|
-
const EXPORT_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/u;
|
|
588
|
-
/**
|
|
589
|
-
* 这个入口是否需要「CJS 包的 ESM 便利入口」的 default 补偿。判据**只看 package.json 的声明**:
|
|
590
|
-
*
|
|
591
|
-
* - `type: "module"` 的包:Node 与我们都按 `exports`(或 `main` 本身就是 ESM)解析,两边一致,不补;
|
|
592
|
-
* - 其余是 CJS 包:`module` 只是打包器约定(视为 ESM),`main` 才是 CJS。这条链选 `module`(更小、没有
|
|
593
|
-
* webpack 垫片),而 Node 的 `import pkg from 'pkg'` 落到 `main`、default 是它的 `module.exports`——
|
|
594
|
-
* 所以浏览器侧得补一个 default,否则消费方那句 default import 直接报
|
|
595
|
-
* `does not provide an export named 'default'`(`rrule` 就是这一例)。
|
|
596
|
-
* - 有 `exports` 的包两边都按条件解析,不涉及这条约定;入口不是 `module` 的(选了 `main`)由
|
|
597
|
-
* {@link cjsShimOf} 处理。`main`/`module` 只描述根出口,子路径不在此列。
|
|
598
|
-
*/
|
|
599
|
-
async function needsCjsInterop(entry, exportName) {
|
|
600
|
-
if (exportName !== ROOT_EXPORT) return false;
|
|
601
|
-
const manifestPath = await packageJsonNear(entry);
|
|
602
|
-
const manifest = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{}"));
|
|
603
|
-
if (manifest.type === "module" || manifest.exports !== void 0) return false;
|
|
604
|
-
const manifestModule = manifest.module;
|
|
605
|
-
if (manifestModule === void 0 || manifest.main === void 0) return false;
|
|
606
|
-
const modulePath = resolve(dirname(manifestPath), manifestModule);
|
|
607
|
-
return await realpath(modulePath).catch(() => modulePath) === await realpath(entry).catch(() => resolve(entry)) || modulePath === resolve(entry);
|
|
608
|
-
}
|
|
609
|
-
/**
|
|
610
|
-
* 给「CJS 包的 ESM 便利入口」补 default:**看产物的真实导出面**(`chunk.exports`),不猜源码。
|
|
611
|
-
* 已经有 default 的(`jsbi` 的 `export default JSBI`)原样保留——补成对象会把它顶掉,
|
|
612
|
-
* `@js-temporal/polyfill` 那句 `import e from "jsbi"` 于是拿到命名空间、运行期 `e.BigInt is not a function`;
|
|
613
|
-
* 没有的补成 `{ 具名导出… }`,与 CJS 的 `module.exports` 同形(`rrule` 的 `ALL_WEEKDAYS` / `rrulestr` 那批)。
|
|
614
|
-
*/
|
|
615
|
-
function cjsInteropPlugin(exportName) {
|
|
616
794
|
return {
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
for (const chunk of Object.values(bundle)) {
|
|
620
|
-
if (chunk.type !== "chunk" || chunk.isEntry !== true || chunk.name !== exportName) continue;
|
|
621
|
-
if (chunk.exports.includes("default")) continue;
|
|
622
|
-
const named = chunk.exports.filter((name) => EXPORT_IDENTIFIER.test(name));
|
|
623
|
-
if (named.length === 0) continue;
|
|
624
|
-
chunk.code += `\nexport default { ${named.join(", ")} };\n`;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
795
|
+
id,
|
|
796
|
+
code
|
|
627
797
|
};
|
|
628
798
|
}
|
|
629
799
|
/**
|
|
630
|
-
* CSS
|
|
631
|
-
*
|
|
632
|
-
*
|
|
800
|
+
* CSS 出口的产物:**就是 CSS 文本**(不包装成 JS、不注入 `<style>`)。
|
|
801
|
+
*
|
|
802
|
+
* JS 里的 `import "x.css"` 是另一条路(`cssInlinePlugins` 在构建期内联成注入样式的模块),与本函数无关。
|
|
803
|
+
* 这里的 `url()` 指向包内资产:读到字节、按内容 hash 落到 `assets/`、改写成联邦地址——否则浏览器会按
|
|
804
|
+
* CSS 的相对路径去请求 `.pnpm` 里的文件,路由按清单服务就是 404。
|
|
805
|
+
*
|
|
806
|
+
* `lightningcss` 的 visitor 是同步回调,所以分两遍:第一遍只收集相对 `url()`,异步读资产/算 hash;
|
|
807
|
+
* 第二遍按映射改写(全程异步 IO)。
|
|
633
808
|
*
|
|
634
|
-
* @
|
|
809
|
+
* @param options - 入口文件、出口名、缓存目录、包键、产物清单。
|
|
635
810
|
*/
|
|
636
|
-
async function
|
|
637
|
-
|
|
638
|
-
const
|
|
811
|
+
async function bundleStyleOf(options) {
|
|
812
|
+
const { transform } = await import("lightningcss");
|
|
813
|
+
const source = await readFile(options.entry);
|
|
814
|
+
const relativeUrl = (ref) => ref !== "" && !ref.startsWith("#") && !ref.startsWith("/") && !/^[a-z][a-z0-9+.-]*:/iu.test(ref);
|
|
815
|
+
const refs = /* @__PURE__ */ new Set();
|
|
816
|
+
transform({
|
|
817
|
+
filename: options.entry,
|
|
818
|
+
code: source,
|
|
819
|
+
minify: false,
|
|
820
|
+
visitor: { Url(node) {
|
|
821
|
+
if (relativeUrl(node.url)) refs.add(node.url);
|
|
822
|
+
} }
|
|
823
|
+
});
|
|
824
|
+
const mapping = /* @__PURE__ */ new Map();
|
|
825
|
+
for (const ref of refs) {
|
|
826
|
+
const absolute = join(dirname(options.entry), ref);
|
|
827
|
+
const bytes = await readFile(absolute).catch(() => void 0);
|
|
828
|
+
if (bytes === void 0) continue;
|
|
829
|
+
const extension = extname(absolute);
|
|
830
|
+
const name = `assets/${basename(absolute, extension)}-${createHash("sha256").update(bytes).digest("hex").slice(0, 8)}${extension}`;
|
|
831
|
+
mapping.set(ref, `${CLIENT_MODULES_PREFIX}/${options.key}/${name}`);
|
|
832
|
+
await mkdir(dirname(join(options.outDir, name)), { recursive: true });
|
|
833
|
+
await writeFile(join(options.outDir, name), bytes);
|
|
834
|
+
options.files[name] = `${options.key}/${name}`;
|
|
835
|
+
}
|
|
836
|
+
const result = transform({
|
|
837
|
+
filename: options.entry,
|
|
838
|
+
code: source,
|
|
839
|
+
minify: false,
|
|
840
|
+
visitor: { Url(node) {
|
|
841
|
+
const rewritten = mapping.get(node.url);
|
|
842
|
+
return rewritten === void 0 ? void 0 : {
|
|
843
|
+
...node,
|
|
844
|
+
url: rewritten
|
|
845
|
+
};
|
|
846
|
+
} }
|
|
847
|
+
});
|
|
848
|
+
const file = join(options.outDir, options.exportName);
|
|
639
849
|
await mkdir(dirname(file), { recursive: true });
|
|
640
|
-
await writeFile(file,
|
|
641
|
-
|
|
850
|
+
await writeFile(file, result.code);
|
|
851
|
+
options.files[options.exportName] = `${options.key}/${options.exportName}`;
|
|
642
852
|
}
|
|
643
853
|
/**
|
|
644
854
|
* 确保该包的产物在(指纹没变、且**这次要的出口**都在就直接用)。
|
|
@@ -723,7 +933,8 @@ async function bundlePackageOnce(options, key) {
|
|
|
723
933
|
const outDir = join(options.cacheRoot, key);
|
|
724
934
|
const manifestPath = join(outDir, ".files.json");
|
|
725
935
|
options.log?.(`[bundle-rev] ${options.name}: hash-start watch=${options.watch.join(",")}\n`);
|
|
726
|
-
const
|
|
936
|
+
const rootRev = createHash("sha1").update(dirname(options.watch[0] ?? options.manifestOf ?? "")).digest("hex").slice(0, 8);
|
|
937
|
+
const rev = `${BUNDLER_REV}:${rootRev}:${await fingerprint(options.watch)}`;
|
|
727
938
|
options.log?.(`[bundle-rev] ${options.name}: hash=${rev}\n`);
|
|
728
939
|
const cached = JSON.parse(await readFile(manifestPath, "utf8").catch(() => "{}"));
|
|
729
940
|
const fresh = cached.rev === rev;
|
|
@@ -740,34 +951,63 @@ async function bundlePackageOnce(options, key) {
|
|
|
740
951
|
for (const { find, replacement } of options.alias) if (typeof find === "string" ? source.startsWith(find) : find.test(source)) return source.replace(find, replacement);
|
|
741
952
|
}
|
|
742
953
|
},
|
|
743
|
-
browserProcessPlugin(),
|
|
954
|
+
browserProcessPlugin(await clientBuildMeta(options.repoRoot)),
|
|
955
|
+
{
|
|
956
|
+
name: "dsh-css-specifiers",
|
|
957
|
+
resolveId(source, importer) {
|
|
958
|
+
if (!source.endsWith(".css") || source.startsWith(".") || source.startsWith("/")) return;
|
|
959
|
+
try {
|
|
960
|
+
return createRequire(importer ?? join(options.repoRoot, "package.json")).resolve(source);
|
|
961
|
+
} catch {
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
},
|
|
744
966
|
cssInlinePlugins({
|
|
745
967
|
name: key,
|
|
746
968
|
assets: "threshold",
|
|
747
969
|
assetUrl: (file) => `${CLIENT_MODULES_PREFIX}/${key}/${file}`
|
|
748
970
|
}),
|
|
749
971
|
clientAssetPlugin({ assets: "threshold" }),
|
|
750
|
-
esmExternalRequirePlugin({ external: [...resolvable.keys()].filter((name) => !NODE_BUILTIN.test(name)).map((name) => barePackageRegExp(name)) })
|
|
972
|
+
esmExternalRequirePlugin({ external: [...resolvable.keys()].filter((name) => !NODE_BUILTIN.test(name) && name !== options.name).map((name) => barePackageRegExp(name)) })
|
|
751
973
|
];
|
|
752
974
|
const startedAt = Date.now();
|
|
753
975
|
options.log?.(`[bundle-build] ${options.name}: rolldown start\n`);
|
|
754
976
|
const input = {};
|
|
755
|
-
|
|
977
|
+
const entryShims = /* @__PURE__ */ new Map();
|
|
978
|
+
const styleEntries = /* @__PURE__ */ new Map();
|
|
756
979
|
for (const [exportName, entry] of Object.entries(options.input)) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
980
|
+
const kind = await moduleKindOf(entry);
|
|
981
|
+
if (kind === "cjs") {
|
|
982
|
+
const shim = await cjsShimOf({
|
|
983
|
+
name: options.name,
|
|
984
|
+
exportName,
|
|
985
|
+
entry,
|
|
986
|
+
from: entry,
|
|
987
|
+
...options.log === void 0 ? {} : { log: options.log }
|
|
988
|
+
});
|
|
989
|
+
if (shim !== void 0) entryShims.set(shim.id, shim.code);
|
|
990
|
+
input[exportName] = shim?.id ?? entry;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (kind === "style") {
|
|
994
|
+
styleEntries.set(exportName, entry);
|
|
995
|
+
const styleId = `\0dsh-style:${options.name}:${exportName}:entry`;
|
|
996
|
+
entryShims.set(styleId, "export {};\n");
|
|
997
|
+
input[exportName] = styleId;
|
|
998
|
+
continue;
|
|
999
|
+
}
|
|
1000
|
+
input[exportName] = entry;
|
|
769
1001
|
}
|
|
770
|
-
if (
|
|
1002
|
+
if (entryShims.size !== 0) plugins.push({
|
|
1003
|
+
name: "dsh-entry-shims",
|
|
1004
|
+
resolveId(source) {
|
|
1005
|
+
return entryShims.has(source) ? source : void 0;
|
|
1006
|
+
},
|
|
1007
|
+
load(source) {
|
|
1008
|
+
return entryShims.get(source);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
771
1011
|
const bundle = await rolldown({
|
|
772
1012
|
input,
|
|
773
1013
|
plugins,
|
|
@@ -784,10 +1024,7 @@ async function bundlePackageOnce(options, key) {
|
|
|
784
1024
|
"safari16.4"
|
|
785
1025
|
],
|
|
786
1026
|
decorator: { legacy: true },
|
|
787
|
-
define: {
|
|
788
|
-
"process.env": "{ env: {} }",
|
|
789
|
-
process: "{ env: {} }"
|
|
790
|
-
}
|
|
1027
|
+
define: { process: "{ env: {} }" }
|
|
791
1028
|
}
|
|
792
1029
|
});
|
|
793
1030
|
const { output: items } = await bundle.generate({
|
|
@@ -807,8 +1044,10 @@ async function bundlePackageOnce(options, key) {
|
|
|
807
1044
|
return;
|
|
808
1045
|
}
|
|
809
1046
|
await mkdir(outDir, { recursive: true });
|
|
1047
|
+
const styleExports = new Set(styleEntries.keys());
|
|
810
1048
|
for (const item of items) {
|
|
811
1049
|
if (item.type !== "chunk") continue;
|
|
1050
|
+
if (item.isEntry && styleExports.has(item.name)) continue;
|
|
812
1051
|
await mkdir(dirname(join(outDir, item.fileName)), { recursive: true });
|
|
813
1052
|
await writeFile(join(outDir, item.fileName), item.code, "utf8");
|
|
814
1053
|
files[item.isEntry ? item.name : item.fileName] = `${key}/${item.fileName}`;
|
|
@@ -819,6 +1058,13 @@ async function bundlePackageOnce(options, key) {
|
|
|
819
1058
|
await writeFile(join(outDir, asset.fileName), asset.source ?? "");
|
|
820
1059
|
files[asset.fileName] = `${key}/${asset.fileName}`;
|
|
821
1060
|
}
|
|
1061
|
+
for (const [exportName, entry] of styleEntries) await bundleStyleOf({
|
|
1062
|
+
entry,
|
|
1063
|
+
exportName,
|
|
1064
|
+
outDir,
|
|
1065
|
+
key,
|
|
1066
|
+
files
|
|
1067
|
+
});
|
|
822
1068
|
await writeFile(manifestPath, `${JSON.stringify({
|
|
823
1069
|
rev,
|
|
824
1070
|
files
|
|
@@ -1084,11 +1330,7 @@ async function startViteDev(ctx, options) {
|
|
|
1084
1330
|
const server = await createServer({
|
|
1085
1331
|
root: appRoot,
|
|
1086
1332
|
base: "/",
|
|
1087
|
-
define: {
|
|
1088
|
-
"process.env.NODE_ENV": JSON.stringify(process.env["NODE_ENV"] ?? "development"),
|
|
1089
|
-
"process.env": "{ env: {} }",
|
|
1090
|
-
process: "{ env: {} }"
|
|
1091
|
-
},
|
|
1333
|
+
define: { process: "{ env: {} }" },
|
|
1092
1334
|
publicDir: false,
|
|
1093
1335
|
appType: "mpa",
|
|
1094
1336
|
configFile: false,
|
|
@@ -1113,7 +1355,7 @@ async function startViteDev(ctx, options) {
|
|
|
1113
1355
|
noDiscovery: true,
|
|
1114
1356
|
include: []
|
|
1115
1357
|
},
|
|
1116
|
-
plugins: [dshClientRowsPlugin({
|
|
1358
|
+
plugins: [browserProcessPlugin(await clientBuildMeta(repoRoot)), dshClientRowsPlugin({
|
|
1117
1359
|
repoRoot,
|
|
1118
1360
|
rows: () => rowsOf(options.rows),
|
|
1119
1361
|
resolvePluginUrl: options.resolvePluginUrl,
|
|
@@ -1351,6 +1593,7 @@ function apply(ctx) {
|
|
|
1351
1593
|
*/
|
|
1352
1594
|
const depsUrlOfSpecifier = async (specifier, importer) => {
|
|
1353
1595
|
if (specifier.startsWith("@/")) return void 0;
|
|
1596
|
+
if (specifier.endsWith(".css")) return void 0;
|
|
1354
1597
|
if (NODE_BUILTIN.test(specifier)) return void 0;
|
|
1355
1598
|
const bare = packageNameOf(specifier) || specifier;
|
|
1356
1599
|
const root = await rootOf(bare);
|
|
@@ -1437,8 +1680,9 @@ function apply(ctx) {
|
|
|
1437
1680
|
if (code === void 0) return false;
|
|
1438
1681
|
const cacheControl = root !== void 0 && !root.includes("node_modules") ? "no-cache" : "public, max-age=31536000, immutable";
|
|
1439
1682
|
const etag = `"${bundled.rev}"`;
|
|
1683
|
+
const extension = target.slice(target.lastIndexOf(".") + 1).toLowerCase();
|
|
1440
1684
|
res.writeHead(200, {
|
|
1441
|
-
"content-type": "text/javascript; charset=utf-8",
|
|
1685
|
+
"content-type": extension === "css" ? "text/css; charset=utf-8" : "text/javascript; charset=utf-8",
|
|
1442
1686
|
"cache-control": cacheControl,
|
|
1443
1687
|
etag
|
|
1444
1688
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morlay/dsh-client-modules",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
4
4
|
"description": "Dev-form web layer for dsh: serves the page, the app entry and every client resource through an in-process vite dev server (middleware mode) — the node-side counterpart of upstream @deepseek-ai/dsh-client-modules, with the module table replaced by vite's resolver, dependency prebundling and HMR.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"client",
|
|
@@ -36,26 +36,26 @@
|
|
|
36
36
|
"./locale/*.json": "./locale/*.json"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@deepseek-ai/dsh-client-ui-theme": "^0.2.2-
|
|
40
|
-
"@deepseek-ai/dsh-host-webserver": "^0.2.2-
|
|
41
|
-
"@morlay/dsh-client-ui-primitives": "0.0.
|
|
42
|
-
"@morlay/dsh-cordis-assets": "0.2.
|
|
39
|
+
"@deepseek-ai/dsh-client-ui-theme": "^0.2.2-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-host-webserver": "^0.2.2-rc.1",
|
|
41
|
+
"@morlay/dsh-client-ui-primitives": "0.0.15",
|
|
42
|
+
"@morlay/dsh-cordis-assets": "0.2.11",
|
|
43
43
|
"@oxc-project/runtime": "^0.153.0",
|
|
44
44
|
"lightningcss": "^1.30.0",
|
|
45
45
|
"vite": "^8.3.1"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@deepseek-ai/cordis": "^4.0
|
|
48
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
49
49
|
"@local/devkit": "0.1.0",
|
|
50
50
|
"@types/node": "^26.5.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@deepseek-ai/cordis": "^4.0
|
|
53
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
54
54
|
"react": "^18.3.1",
|
|
55
55
|
"rolldown": "^1.2.14"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"dsh": ">= 0.2.2-
|
|
58
|
+
"dsh": ">= 0.2.2-rc.1 < 0.2.3",
|
|
59
59
|
"node": ">=24.11.0"
|
|
60
60
|
},
|
|
61
61
|
"dsh": {
|