@morlay/dsh-client-modules 0.2.11-alpha.2 → 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.
- package/dist/client.cjs +115 -93
- package/dist/index.cjs +376 -57
- package/dist/index.mjs +377 -58
- package/package.json +8 -8
- package/src/__tests__/bundle.spec.ts +373 -3
- package/src/__tests__/client-meta.spec.ts +114 -0
- package/src/__tests__/resolve.spec.ts +20 -2
- package/src/bundle.ts +330 -73
- package/src/client-meta.ts +128 -0
- package/src/index.ts +13 -3
- package/src/resolve.ts +2 -1
- package/src/serve-vite.ts +14 -16
|
@@ -7,11 +7,112 @@
|
|
|
7
7
|
import { appendFile, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
|
|
8
8
|
import { tmpdir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
10
11
|
import { afterAll, beforeAll, expect, test } from "vitest";
|
|
11
|
-
import { BUNDLER_REV, bundlePackage, clientModulesAlias, urlOf } from "../bundle.ts";
|
|
12
|
+
import { BUNDLER_REV, bundlePackage, clientModulesAlias, moduleKindOf, urlOf } from "../bundle.ts";
|
|
12
13
|
|
|
13
14
|
const repoRoot = process.cwd();
|
|
14
15
|
|
|
16
|
+
test("CSS 出口:产出 CSS 文本(url() 资产改写 + 落 assets),不生成 JS", async () => {
|
|
17
|
+
const pkg = join(work, "node_modules", "style-pkg");
|
|
18
|
+
await mkdir(join(pkg, "dist"), { recursive: true });
|
|
19
|
+
await writeFile(
|
|
20
|
+
join(pkg, "package.json"),
|
|
21
|
+
`${JSON.stringify({
|
|
22
|
+
name: "style-pkg",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
exports: { "./theme.css": "./dist/theme.css" },
|
|
25
|
+
})}\n`,
|
|
26
|
+
"utf8",
|
|
27
|
+
);
|
|
28
|
+
await writeFile(join(pkg, "dist", "theme.css"), ".icon { background: url(./dot.svg) }\n", "utf8");
|
|
29
|
+
await writeFile(join(pkg, "dist", "dot.svg"), "<svg/>\n", "utf8");
|
|
30
|
+
|
|
31
|
+
const bundled = await bundlePackage({
|
|
32
|
+
cacheRoot,
|
|
33
|
+
repoRoot: work,
|
|
34
|
+
alias: [],
|
|
35
|
+
name: "style-pkg",
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
input: { "theme.css": join(pkg, "dist", "theme.css") },
|
|
38
|
+
watch: [pkg],
|
|
39
|
+
});
|
|
40
|
+
const files = [...(bundled?.files.keys() ?? [])];
|
|
41
|
+
expect(files).toContain("theme.css");
|
|
42
|
+
const asset = files.find((file) => file.startsWith("assets/dot-"));
|
|
43
|
+
expect(asset).toBeDefined();
|
|
44
|
+
const css = await readFile(join(cacheRoot, bundled?.files.get("theme.css") ?? ""), "utf8");
|
|
45
|
+
expect(css).toContain(`/@client-modules/style-pkg@1.0.0/${asset ?? ""}`);
|
|
46
|
+
await expect(stat(join(cacheRoot, "style-pkg@1.0.0", "theme.css.js"))).rejects.toThrow();
|
|
47
|
+
await expect(
|
|
48
|
+
readFile(join(cacheRoot, bundled?.files.get(asset ?? "") ?? ""), "utf8"),
|
|
49
|
+
).resolves.toContain("<svg");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("模块类型:扩展名 + 最近的 package.json(什么时候 ESM、什么时候 CJS)", async () => {
|
|
53
|
+
const root = join(work, "kinds");
|
|
54
|
+
await mkdir(join(root, "esm"), { recursive: true });
|
|
55
|
+
await writeFile(
|
|
56
|
+
join(root, "package.json"),
|
|
57
|
+
`${JSON.stringify({ name: "kinds", version: "1.0.0", type: "commonjs" })}\n`,
|
|
58
|
+
"utf8",
|
|
59
|
+
);
|
|
60
|
+
const touch = async (file: string): Promise<string> => {
|
|
61
|
+
const path = join(root, file);
|
|
62
|
+
await writeFile(path, "", "utf8");
|
|
63
|
+
return path;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// 扩展名就能定。
|
|
67
|
+
expect(await moduleKindOf(await touch("a.mjs"))).toBe("esm");
|
|
68
|
+
expect(await moduleKindOf(await touch("a.mts"))).toBe("esm");
|
|
69
|
+
expect(await moduleKindOf(await touch("a.cjs"))).toBe("cjs");
|
|
70
|
+
expect(await moduleKindOf(await touch("a.cts"))).toBe("cjs");
|
|
71
|
+
expect(await moduleKindOf(await touch("a.ts"))).toBe("source");
|
|
72
|
+
expect(await moduleKindOf(await touch("a.tsx"))).toBe("source");
|
|
73
|
+
expect(await moduleKindOf(await touch("a.css"))).toBe("style");
|
|
74
|
+
expect(await moduleKindOf(await touch("a.png"))).toBe("asset");
|
|
75
|
+
|
|
76
|
+
// `.js`:`type` 与**语法检测**(Node 的 `--experimental-detect-module`)。
|
|
77
|
+
expect(await moduleKindOf(await touch("a.js"))).toBe("cjs");
|
|
78
|
+
await writeFile(
|
|
79
|
+
join(root, "esm", "package.json"),
|
|
80
|
+
`${JSON.stringify({ name: "kinds-esm", version: "1.0.0", type: "module" })}\n`,
|
|
81
|
+
"utf8",
|
|
82
|
+
);
|
|
83
|
+
expect(await moduleKindOf(await touch("esm/a.js"))).toBe("esm");
|
|
84
|
+
|
|
85
|
+
const legacy = join(root, "legacy");
|
|
86
|
+
await mkdir(join(legacy, "esm"), { recursive: true });
|
|
87
|
+
await mkdir(join(legacy, "cjs"), { recursive: true });
|
|
88
|
+
await writeFile(
|
|
89
|
+
join(legacy, "package.json"),
|
|
90
|
+
`${JSON.stringify({
|
|
91
|
+
name: "kinds-legacy",
|
|
92
|
+
version: "1.0.0",
|
|
93
|
+
main: "cjs/index.js",
|
|
94
|
+
module: "esm/index.js",
|
|
95
|
+
})}\n`,
|
|
96
|
+
"utf8",
|
|
97
|
+
);
|
|
98
|
+
// 没有 `type`:看源码里有没有 ESM 语法。
|
|
99
|
+
await writeFile(join(legacy, "esm", "index.js"), "export const x = 1;\n", "utf8");
|
|
100
|
+
await writeFile(join(legacy, "cjs", "index.js"), "module.exports = 1;\n", "utf8");
|
|
101
|
+
expect(await moduleKindOf(join(legacy, "esm", "index.js"))).toBe("esm");
|
|
102
|
+
expect(await moduleKindOf(join(legacy, "cjs", "index.js"))).toBe("cjs");
|
|
103
|
+
|
|
104
|
+
// `type: "commonjs"` 时不做语法检测(Node 的规则:显式声明优先)。
|
|
105
|
+
const forced = join(root, "forced-cjs");
|
|
106
|
+
await mkdir(forced, { recursive: true });
|
|
107
|
+
await writeFile(
|
|
108
|
+
join(forced, "package.json"),
|
|
109
|
+
`${JSON.stringify({ name: "forced-cjs", version: "1.0.0", type: "commonjs" })}\n`,
|
|
110
|
+
"utf8",
|
|
111
|
+
);
|
|
112
|
+
await writeFile(join(forced, "index.js"), "export const x = 1;\n", "utf8");
|
|
113
|
+
expect(await moduleKindOf(join(forced, "index.js"))).toBe("cjs");
|
|
114
|
+
});
|
|
115
|
+
|
|
15
116
|
test("包级引用的出口按「是不是行」分:行 → client.js,普通 npm 包 → 根出口 index.js", () => {
|
|
16
117
|
const tables = {
|
|
17
118
|
versions: new Map([
|
|
@@ -82,6 +183,7 @@ beforeAll(async () => {
|
|
|
82
183
|
"utf8",
|
|
83
184
|
);
|
|
84
185
|
await writeFile(join(packageRoot, "src", "b.ts"), "export const b = 2;\n", "utf8");
|
|
186
|
+
await writeFile(join(packageRoot, "src", "c.ts"), "export const c = 3;\n", "utf8");
|
|
85
187
|
});
|
|
86
188
|
|
|
87
189
|
afterAll(async () => {
|
|
@@ -154,6 +256,23 @@ test("装饰器在产物里必须已被转译,且**不依赖进程 cwd**", asy
|
|
|
154
256
|
}
|
|
155
257
|
});
|
|
156
258
|
|
|
259
|
+
test("tsconfig 与 `transform.decorator` 都声明装饰器时不再提示冲突(语义相同,口径取 transform 那侧)", async () => {
|
|
260
|
+
// 两处声明是必须的:tsconfig 那项管不到 `node_modules` 里的包(include 面默认排除它),transform 那侧才是
|
|
261
|
+
// 保底。rolldown 的 `CONFIGURATION_FIELD_CONFLICT` 只是"重复声明"级提示,本链在 `onLog` 里过滤掉——
|
|
262
|
+
// 这条用例钉住"过滤生效",别让它哪天又冒出来。
|
|
263
|
+
const warned: string[] = [];
|
|
264
|
+
const original = console.warn;
|
|
265
|
+
console.warn = (...args: unknown[]) => {
|
|
266
|
+
warned.push(args.map(String).join(" "));
|
|
267
|
+
};
|
|
268
|
+
try {
|
|
269
|
+
await bundlePackage({ ...options("c"), tsconfig: join(work, "tsconfig.json") });
|
|
270
|
+
} finally {
|
|
271
|
+
console.warn = original;
|
|
272
|
+
}
|
|
273
|
+
expect(warned.filter((line) => line.includes("CONFIGURATION_FIELD_CONFLICT"))).toEqual([]);
|
|
274
|
+
});
|
|
275
|
+
|
|
157
276
|
test("包住在没有 tsconfig 祖先的地方时,`resolve.tsconfigFilename` 也能让它转", async () => {
|
|
158
277
|
// 外置/官方的包就住在 `node_modules/.pnpm/…` 里——向上找不到任何 tsconfig。这条守着那个开关:
|
|
159
278
|
// 只给 `transform.decorator`(实测 vite 不带进 rolldown)不行,`resolve.tsconfigFilename` 才行。
|
|
@@ -172,8 +291,9 @@ test("包住在没有 tsconfig 祖先的地方时,`resolve.tsconfigFilename`
|
|
|
172
291
|
].join("\n"),
|
|
173
292
|
"utf8",
|
|
174
293
|
);
|
|
175
|
-
//
|
|
176
|
-
// `
|
|
294
|
+
// 包在 `work2/node_modules/…`(祖先链里没有 tsconfig),tsconfig 由 `resolve.tsconfigFilename` 显式指过去——
|
|
295
|
+
// 外置/官方包在产物里的处境(`dshdev --prod` 的闭包面就是 node_modules 形态)。**装饰器由 transform 那一侧
|
|
296
|
+
// 保底**(tsconfig 的 include 面默认排除 node_modules),这条用例同时钉住"指了 tsconfig 也不会把装饰器弄丢"。
|
|
177
297
|
const elsewhere = join(work2, "..", `dsh-tsconfig-${String(Date.now())}`, "tsconfig.json");
|
|
178
298
|
await mkdir(join(elsewhere, ".."), { recursive: true });
|
|
179
299
|
await writeFile(
|
|
@@ -202,6 +322,256 @@ test("包住在没有 tsconfig 祖先的地方时,`resolve.tsconfigFilename`
|
|
|
202
322
|
}
|
|
203
323
|
});
|
|
204
324
|
|
|
325
|
+
test("CJS/UMD 入口(webpack 包装、局部名与导出名不同):具名导出与 default 同时产出", async () => {
|
|
326
|
+
// 三方包按 **node 的解析**取入口(`main`),老包(`rrule` / `@xterm/xterm` / `@xterm/addon-fit`)进来的
|
|
327
|
+
// 就是 CJS/UMD——导出面是**运行期**的,rolldown 只能给出 `export default module.exports`。消费方还要
|
|
328
|
+
// 具名导入(`import { Terminal } from '@xterm/xterm'`),所以入口求值后按键补成 `export const <键>`。
|
|
329
|
+
//
|
|
330
|
+
// 这里用 **webpack 风格 UMD**(`module.exports = factory()`)且局部名与导出名不同:产物里不能出现
|
|
331
|
+
// `export default { FitAddon }` 这种引用裸导出名的写法——局部作用域里没有它,浏览器会报
|
|
332
|
+
// `ReferenceError: FitAddon is not defined`(`@xterm/addon-fit@0.11.0` 真实踩到的就是这条)。
|
|
333
|
+
const cjs = join(work, "cjs-entry");
|
|
334
|
+
await mkdir(join(cjs, "dist"), { recursive: true });
|
|
335
|
+
await writeFile(
|
|
336
|
+
join(cjs, "package.json"),
|
|
337
|
+
`${JSON.stringify({ name: "cjs-entry", version: "1.0.0", main: "dist/index.js" })}\n`,
|
|
338
|
+
"utf8",
|
|
339
|
+
);
|
|
340
|
+
await writeFile(
|
|
341
|
+
join(cjs, "dist", "index.js"),
|
|
342
|
+
[
|
|
343
|
+
"!function (root, factory) {",
|
|
344
|
+
' if (typeof exports === "object" && typeof module === "object") module.exports = factory();',
|
|
345
|
+
' else if (typeof define === "function" && define.amd) define([], factory);',
|
|
346
|
+
" else root.FitAddon = factory();",
|
|
347
|
+
"}(globalThis, function () {",
|
|
348
|
+
' "use strict";',
|
|
349
|
+
" var t = {};",
|
|
350
|
+
" t.FitAddon = class FitAddon {};",
|
|
351
|
+
// zod@4 的 CJS 面就有这种键:`export const function = …` 是语法错误,必须被剔掉。
|
|
352
|
+
" t.function = 1;",
|
|
353
|
+
" return t;",
|
|
354
|
+
"});",
|
|
355
|
+
"",
|
|
356
|
+
].join("\n"),
|
|
357
|
+
"utf8",
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const bundled = await bundlePackage({
|
|
361
|
+
cacheRoot,
|
|
362
|
+
repoRoot: work,
|
|
363
|
+
alias: [],
|
|
364
|
+
name: "cjs-entry",
|
|
365
|
+
version: "1.0.0",
|
|
366
|
+
input: { index: join(cjs, "dist", "index.js") },
|
|
367
|
+
watch: [join(cjs, "dist")],
|
|
368
|
+
});
|
|
369
|
+
const file = join(cacheRoot, bundled?.files.get("index") ?? "");
|
|
370
|
+
const loaded = (await import(pathToFileURL(file).href)) as {
|
|
371
|
+
readonly FitAddon?: unknown;
|
|
372
|
+
readonly default?: { readonly FitAddon?: unknown };
|
|
373
|
+
};
|
|
374
|
+
// 两种写法都成立:具名导入(浏览器里 `import { FitAddon }`)与 default(Node/CJS 的 `module.exports`)。
|
|
375
|
+
expect(typeof loaded.FitAddon).toBe("function");
|
|
376
|
+
expect(typeof loaded.default?.FitAddon).toBe("function");
|
|
377
|
+
|
|
378
|
+
// shim 只活在内存里(虚拟入口),缓存目录不落这个文件。
|
|
379
|
+
await expect(stat(join(cacheRoot, "cjs-entry@1.0.0", "index.cjs-shim.mjs"))).rejects.toThrow();
|
|
380
|
+
// 保留字不能出现在 `export const` 里(语法错误)。
|
|
381
|
+
expect(await readFile(file, "utf8")).not.toMatch(/export const function\b/u);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("有 ESM 入口的包:不生成 CJS shim(`zod@4` 那种 import/require 双支)", async () => {
|
|
385
|
+
// 入口由清单的 `import` 条件选出(ESM),就交给 rolldown 直出;只有 node 求值确认是 CJS 的入口才包 shim。
|
|
386
|
+
const esm = join(work, "dual-esm");
|
|
387
|
+
await mkdir(join(esm, "dist"), { recursive: true });
|
|
388
|
+
await writeFile(
|
|
389
|
+
join(esm, "package.json"),
|
|
390
|
+
`${JSON.stringify({
|
|
391
|
+
name: "dual-esm",
|
|
392
|
+
version: "1.0.0",
|
|
393
|
+
type: "module",
|
|
394
|
+
exports: { ".": { import: "./dist/index.js", require: "./dist/index.cjs" } },
|
|
395
|
+
})}\n`,
|
|
396
|
+
"utf8",
|
|
397
|
+
);
|
|
398
|
+
await writeFile(join(esm, "dist", "index.js"), "export const z = 1;\n", "utf8");
|
|
399
|
+
await writeFile(join(esm, "dist", "index.cjs"), "module.exports = { z: 1 };\n", "utf8");
|
|
400
|
+
|
|
401
|
+
const bundled = await bundlePackage({
|
|
402
|
+
cacheRoot,
|
|
403
|
+
repoRoot: work,
|
|
404
|
+
alias: [],
|
|
405
|
+
name: "dual-esm",
|
|
406
|
+
version: "1.0.0",
|
|
407
|
+
input: { index: join(esm, "dist", "index.js") },
|
|
408
|
+
watch: [join(esm, "dist")],
|
|
409
|
+
});
|
|
410
|
+
expect([...(bundled?.files.keys() ?? [])]).toEqual(["index"]);
|
|
411
|
+
await expect(stat(join(cacheRoot, "dual-esm@1.0.0", "index.cjs-shim.mjs"))).rejects.toThrow();
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("ESM 入口:交给 rolldown 的具名导出,不补任何 default", async () => {
|
|
415
|
+
// node 的 `require(esm)` 会返回命名空间(`[object Module]`)。这种入口不该被当成 CJS 包补 default:
|
|
416
|
+
// rolldown 自己就能产出具名导出,硬补一个 `default = 命名空间` 只会多一个消费方不该依赖的面。
|
|
417
|
+
const esm = join(work, "esm-entry");
|
|
418
|
+
await mkdir(join(esm, "src"), { recursive: true });
|
|
419
|
+
await writeFile(
|
|
420
|
+
join(esm, "package.json"),
|
|
421
|
+
`${JSON.stringify({ name: "esm-entry", version: "1.0.0", type: "module", main: "src/index.js" })}\n`,
|
|
422
|
+
"utf8",
|
|
423
|
+
);
|
|
424
|
+
await writeFile(
|
|
425
|
+
join(esm, "src", "index.js"),
|
|
426
|
+
"const t = class {};\nexport { t as Terminal };\n",
|
|
427
|
+
"utf8",
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const bundled = await bundlePackage({
|
|
431
|
+
cacheRoot,
|
|
432
|
+
repoRoot: work,
|
|
433
|
+
alias: [],
|
|
434
|
+
name: "esm-entry",
|
|
435
|
+
version: "1.0.0",
|
|
436
|
+
input: { index: join(esm, "src", "index.js") },
|
|
437
|
+
watch: [join(esm, "src")],
|
|
438
|
+
});
|
|
439
|
+
const file = join(cacheRoot, bundled?.files.get("index") ?? "");
|
|
440
|
+
const code = await readFile(file, "utf8");
|
|
441
|
+
expect(code).not.toMatch(/export default/u);
|
|
442
|
+
const loaded = (await import(pathToFileURL(file).href)) as { readonly Terminal?: unknown };
|
|
443
|
+
expect(typeof loaded.Terminal).toBe("function");
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
test("exports 落点 + 扩展名/type:`.cjs` 才 shim,`.mjs` 与 `module` 字段指向的 `.js` 都不 shim", async () => {
|
|
447
|
+
// 条件决定"哪个文件",扩展名/type 决定"那个文件是什么模块"。两边都处理到,才是完整判定。
|
|
448
|
+
const cond = join(work, "cond-entry");
|
|
449
|
+
await mkdir(join(cond, "dist"), { recursive: true });
|
|
450
|
+
await writeFile(
|
|
451
|
+
join(cond, "package.json"),
|
|
452
|
+
`${JSON.stringify({
|
|
453
|
+
name: "cond-entry",
|
|
454
|
+
version: "1.0.0",
|
|
455
|
+
exports: { ".": { import: "./dist/index.mjs", require: "./dist/index.cjs" } },
|
|
456
|
+
})}\n`,
|
|
457
|
+
"utf8",
|
|
458
|
+
);
|
|
459
|
+
await writeFile(join(cond, "dist", "index.mjs"), "export const esm = 1;\n", "utf8");
|
|
460
|
+
await writeFile(join(cond, "dist", "index.cjs"), "module.exports = { cjs: 1 };\n", "utf8");
|
|
461
|
+
|
|
462
|
+
const asCjs = await bundlePackage({
|
|
463
|
+
cacheRoot,
|
|
464
|
+
repoRoot: work,
|
|
465
|
+
alias: [],
|
|
466
|
+
name: "cond-entry",
|
|
467
|
+
version: "1.0.0",
|
|
468
|
+
input: { index: join(cond, "dist", "index.cjs") },
|
|
469
|
+
watch: [join(cond, "dist")],
|
|
470
|
+
});
|
|
471
|
+
expect(asCjs?.files.has("index")).toBe(true);
|
|
472
|
+
const cjsLoaded = (await import(
|
|
473
|
+
pathToFileURL(join(cacheRoot, asCjs?.files.get("index") ?? "")).href
|
|
474
|
+
)) as { readonly cjs?: number; readonly default?: { readonly cjs?: number } };
|
|
475
|
+
expect(cjsLoaded.cjs).toBe(1);
|
|
476
|
+
expect(cjsLoaded.default?.cjs).toBe(1);
|
|
477
|
+
|
|
478
|
+
// 换一个包名:缓存目录是**包级**的,同一个包的另一次构建不会删掉上一次的 shim。
|
|
479
|
+
const esmPkg = join(work, "cond-esm");
|
|
480
|
+
await mkdir(join(esmPkg, "dist"), { recursive: true });
|
|
481
|
+
await writeFile(
|
|
482
|
+
join(esmPkg, "package.json"),
|
|
483
|
+
`${JSON.stringify({
|
|
484
|
+
name: "cond-esm",
|
|
485
|
+
version: "1.0.0",
|
|
486
|
+
exports: { ".": { import: "./dist/index.mjs", require: "./dist/index.cjs" } },
|
|
487
|
+
})}\n`,
|
|
488
|
+
"utf8",
|
|
489
|
+
);
|
|
490
|
+
await writeFile(join(esmPkg, "dist", "index.mjs"), "export const esm = 1;\n", "utf8");
|
|
491
|
+
await writeFile(join(esmPkg, "dist", "index.cjs"), "module.exports = { cjs: 1 };\n", "utf8");
|
|
492
|
+
const asEsm = await bundlePackage({
|
|
493
|
+
cacheRoot,
|
|
494
|
+
repoRoot: work,
|
|
495
|
+
alias: [],
|
|
496
|
+
name: "cond-esm",
|
|
497
|
+
version: "1.0.0",
|
|
498
|
+
input: { index: join(esmPkg, "dist", "index.mjs") },
|
|
499
|
+
watch: [join(esmPkg, "dist")],
|
|
500
|
+
});
|
|
501
|
+
expect(asEsm?.files.has("index")).toBe(true);
|
|
502
|
+
// ESM 入口交给 rolldown 直出:没有 CJS shim 补出来的 default。
|
|
503
|
+
expect(await readFile(join(cacheRoot, asEsm?.files.get("index") ?? ""), "utf8")).not.toMatch(
|
|
504
|
+
/export default/u,
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
// 老包的 ESM 产物是 `.js`:`module` 字段指向的那份按 ESM 处理(`rrule` 的 dist/esm/index.js)。
|
|
508
|
+
const legacy = join(work, "legacy-module");
|
|
509
|
+
await mkdir(join(legacy, "esm"), { recursive: true });
|
|
510
|
+
await mkdir(join(legacy, "cjs"), { recursive: true });
|
|
511
|
+
await writeFile(
|
|
512
|
+
join(legacy, "package.json"),
|
|
513
|
+
`${JSON.stringify({
|
|
514
|
+
name: "legacy-module",
|
|
515
|
+
version: "1.0.0",
|
|
516
|
+
main: "cjs/index.js",
|
|
517
|
+
module: "esm/index.js",
|
|
518
|
+
})}\n`,
|
|
519
|
+
"utf8",
|
|
520
|
+
);
|
|
521
|
+
await writeFile(join(legacy, "esm", "index.js"), "export const esm = 1;\n", "utf8");
|
|
522
|
+
await writeFile(join(legacy, "cjs", "index.js"), "module.exports = { cjs: 1 };\n", "utf8");
|
|
523
|
+
const legacyEsm = await bundlePackage({
|
|
524
|
+
cacheRoot,
|
|
525
|
+
repoRoot: work,
|
|
526
|
+
alias: [],
|
|
527
|
+
name: "legacy-module",
|
|
528
|
+
version: "1.0.0",
|
|
529
|
+
input: { index: join(legacy, "esm", "index.js") },
|
|
530
|
+
watch: [join(legacy, "esm")],
|
|
531
|
+
});
|
|
532
|
+
expect(legacyEsm?.files.has("index")).toBe(true);
|
|
533
|
+
expect(await readFile(join(cacheRoot, legacyEsm?.files.get("index") ?? ""), "utf8")).not.toMatch(
|
|
534
|
+
/export default/u,
|
|
535
|
+
);
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("扩展名即模块类型:`.mjs` / `.ts` 入口不生成 CJS shim", async () => {
|
|
539
|
+
// `.mjs` 是 ESM,`.ts` 是源码面:都不该进 "CJS 具名导出" 那条求值路径。
|
|
540
|
+
for (const [name, file, code, expected] of [
|
|
541
|
+
["ext-mjs", "index.mjs", "const t = class {};\nexport { t as Terminal };\n", "Terminal"],
|
|
542
|
+
["ext-ts", "index.ts", "export const x = 1;\n", "x"],
|
|
543
|
+
] as const) {
|
|
544
|
+
const dir = join(work, name);
|
|
545
|
+
await mkdir(dir, { recursive: true });
|
|
546
|
+
await writeFile(
|
|
547
|
+
join(dir, "package.json"),
|
|
548
|
+
`${JSON.stringify({ name, version: "1.0.0", type: "module" })}\n`,
|
|
549
|
+
"utf8",
|
|
550
|
+
);
|
|
551
|
+
await writeFile(join(dir, file), code, "utf8");
|
|
552
|
+
const built = await bundlePackage({
|
|
553
|
+
cacheRoot,
|
|
554
|
+
repoRoot: work,
|
|
555
|
+
alias: [],
|
|
556
|
+
name,
|
|
557
|
+
version: "1.0.0",
|
|
558
|
+
input: { index: join(dir, file) },
|
|
559
|
+
watch: [dir],
|
|
560
|
+
});
|
|
561
|
+
expect([...(built?.files.keys() ?? [])]).toEqual(["index"]);
|
|
562
|
+
await expect(stat(join(cacheRoot, `${name}@1.0.0`, "index.cjs-shim.mjs"))).rejects.toThrow();
|
|
563
|
+
const loaded = (await import(
|
|
564
|
+
pathToFileURL(join(cacheRoot, built?.files.get("index") ?? "")).href
|
|
565
|
+
)) as Record<string, unknown>;
|
|
566
|
+
expect(loaded[expected]).toBeDefined();
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("纯 ESM 的包不补 default(不掩盖真实的导出缺失)", async () => {
|
|
571
|
+
const code = await readFile(join(cacheRoot, "fixture@1.0.0", "b.js"), "utf8");
|
|
572
|
+
expect(code).not.toContain("export default");
|
|
573
|
+
});
|
|
574
|
+
|
|
205
575
|
test("清单里解析不到的依赖不算可外置候选,也不该让预编译失败", async () => {
|
|
206
576
|
// 真实里踩到的形状:`zustand` 的 `peerDependencies` 里有 `@types/react`(optional)——类型包不在运行时闭包
|
|
207
577
|
// 里,于是解析它必然失败。它是**可选** peer、又解析不到,两条都不该让它进候选;若把异常抛出去,死的是
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 版本元数据(`DSH_CLIENT_*`):设置页的版本行、侧栏的 `<commit>-dirty` 读它们。取值顺序是
|
|
3
|
+
* 环境(宿主注入)→ `prepare` 写的记录 → 现算,这里三条各钉一条。
|
|
4
|
+
*/
|
|
5
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
9
|
+
import { CLIENT_BUILD_RECORD, clientBuildMeta, clientMetaDefines } from "../client-meta.ts";
|
|
10
|
+
|
|
11
|
+
// 除了那四个注入值,`DEEPSEEK_HARNESS_VERSION` 也要隔离:现算分支优先读它(mise 会往进程里灌),
|
|
12
|
+
// 不隔离的话"没有环境值时现算"这条测的就不是 fixture 了。
|
|
13
|
+
const KEYS = [
|
|
14
|
+
"DSH_CLIENT_VERSION",
|
|
15
|
+
"DSH_CLIENT_COMMIT_HASH",
|
|
16
|
+
"DSH_CLIENT_GIT_DIRTY",
|
|
17
|
+
"DSH_CLIENT_BUILD_PROFILE",
|
|
18
|
+
"DEEPSEEK_HARNESS_VERSION",
|
|
19
|
+
] as const;
|
|
20
|
+
|
|
21
|
+
/** fixture 自己的版本:断言它,而不是某个具体版本号。 */
|
|
22
|
+
const FIXTURE_VERSION = "1.2.3-rc.1";
|
|
23
|
+
const saved = new Map<string, string | undefined>();
|
|
24
|
+
let work: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(async () => {
|
|
27
|
+
for (const key of KEYS) {
|
|
28
|
+
saved.set(key, process.env[key]);
|
|
29
|
+
delete process.env[key];
|
|
30
|
+
}
|
|
31
|
+
work = await mkdtemp(join(tmpdir(), "client-meta-"));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
afterEach(async () => {
|
|
35
|
+
for (const key of KEYS) {
|
|
36
|
+
const value = saved.get(key);
|
|
37
|
+
if (value === undefined) delete process.env[key];
|
|
38
|
+
else process.env[key] = value;
|
|
39
|
+
}
|
|
40
|
+
await rm(work, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("clientMetaDefines", () => {
|
|
44
|
+
it("给真值:每个键换成正文字面量(值原样透传,不在这里定)", () => {
|
|
45
|
+
const meta = { version: "1.2.3-rc.1", commit: "abc1234", dirty: true, profile: "official" };
|
|
46
|
+
expect(clientMetaDefines(meta)).toMatchObject({
|
|
47
|
+
"process.env.DSH_CLIENT_VERSION": JSON.stringify(meta.version),
|
|
48
|
+
"process.env.DSH_CLIENT_COMMIT_HASH": JSON.stringify(meta.commit),
|
|
49
|
+
"process.env.DSH_CLIENT_GIT_DIRTY": JSON.stringify("true"),
|
|
50
|
+
"process.env.DSH_CLIENT_BUILD_PROFILE": JSON.stringify(meta.profile),
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("缺项换成 undefined(前端那几处自己判空),不是字符串 \"undefined\"", () => {
|
|
55
|
+
const defines = clientMetaDefines({});
|
|
56
|
+
expect(defines["process.env.DSH_CLIENT_VERSION"]).toBe("undefined");
|
|
57
|
+
expect(defines["process.env.DSH_CLIENT_COMMIT_HASH"]).toBe("undefined");
|
|
58
|
+
expect(defines["process.env.DSH_CLIENT_GIT_DIRTY"]).toBe("undefined");
|
|
59
|
+
expect(defines["process.env.DSH_CLIENT_BUILD_PROFILE"]).toBe("undefined");
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("clientBuildMeta", () => {
|
|
64
|
+
it("环境里的值优先(dshdev / 桌面壳注入)", async () => {
|
|
65
|
+
process.env["DSH_CLIENT_VERSION"] = "1.2.3-rc.1";
|
|
66
|
+
process.env["DSH_CLIENT_COMMIT_HASH"] = "abc1234";
|
|
67
|
+
process.env["DSH_CLIENT_BUILD_PROFILE"] = "local";
|
|
68
|
+
expect(await clientBuildMeta(work)).toEqual({
|
|
69
|
+
version: "1.2.3-rc.1",
|
|
70
|
+
commit: "abc1234",
|
|
71
|
+
profile: "local",
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("其次是 prepare 写下的记录", async () => {
|
|
76
|
+
await mkdir(join(work, ".dsh-build"), { recursive: true });
|
|
77
|
+
await writeFile(
|
|
78
|
+
join(work, CLIENT_BUILD_RECORD),
|
|
79
|
+
JSON.stringify({
|
|
80
|
+
formatVersion: 1,
|
|
81
|
+
environment: {
|
|
82
|
+
DSH_CLIENT_VERSION: "1.2.3-rc.1",
|
|
83
|
+
DSH_CLIENT_COMMIT_HASH: "abc1234",
|
|
84
|
+
DSH_CLIENT_GIT_DIRTY: "true",
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
"utf8",
|
|
88
|
+
);
|
|
89
|
+
expect(await clientBuildMeta(work)).toEqual({
|
|
90
|
+
version: "1.2.3-rc.1",
|
|
91
|
+
commit: "abc1234",
|
|
92
|
+
dirty: true,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("都没有时现算:只有 package.json 的版本(这个临时目录不是 git 工作树)", async () => {
|
|
97
|
+
await writeFile(
|
|
98
|
+
join(work, "package.json"),
|
|
99
|
+
JSON.stringify({ version: FIXTURE_VERSION }),
|
|
100
|
+
"utf8",
|
|
101
|
+
);
|
|
102
|
+
expect(await clientBuildMeta(work)).toEqual({ version: FIXTURE_VERSION });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("现算也认 `DEEPSEEK_HARNESS_VERSION`(装的是哪一版 harness)", async () => {
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(work, "package.json"),
|
|
108
|
+
JSON.stringify({ version: FIXTURE_VERSION }),
|
|
109
|
+
"utf8",
|
|
110
|
+
);
|
|
111
|
+
process.env["DEEPSEEK_HARNESS_VERSION"] = "9.9.9-other.1";
|
|
112
|
+
expect(await clientBuildMeta(work)).toEqual({ version: "9.9.9-other.1" });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -64,12 +64,30 @@ describe("exportTargetOf", () => {
|
|
|
64
64
|
);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
test("
|
|
67
|
+
test("对象条件的 `import` 优先于 `require`(`zod@4` 的形状:index.js / index.cjs)", async () => {
|
|
68
|
+
const root = join(work, "object-conditions");
|
|
69
|
+
await makePackage(
|
|
70
|
+
root,
|
|
71
|
+
{
|
|
72
|
+
name: "object-conditions",
|
|
73
|
+
version: "1.0.0",
|
|
74
|
+
type: "module",
|
|
75
|
+
exports: {
|
|
76
|
+
".": { types: "./index.d.cts", import: "./index.js", require: "./index.cjs" },
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
{ "index.js": "export const z = 1;\n", "index.cjs": "module.exports = { z: 1 };\n" },
|
|
80
|
+
);
|
|
81
|
+
// 有 ESM 就走 ESM:不要为了跑 CJS 转换而去挑 `require` 那支。
|
|
82
|
+
await expect(exportTargetOf(root, undefined)).resolves.toBe(join(root, "index.js"));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("没有 `exports` 时按打包器约定取 `module`(ESM)→ `main`", async () => {
|
|
68
86
|
const root = join(work, "legacy");
|
|
69
87
|
await makePackage(
|
|
70
88
|
root,
|
|
71
89
|
{ name: "legacy", version: "1.0.0", main: "index.js", module: "esm/index.mjs" },
|
|
72
|
-
{ "esm/index.mjs": "export {};\n" },
|
|
90
|
+
{ "index.js": "module.exports = 1;\n", "esm/index.mjs": "export {};\n" },
|
|
73
91
|
);
|
|
74
92
|
await expect(exportTargetOf(root, undefined)).resolves.toBe(join(root, "esm/index.mjs"));
|
|
75
93
|
});
|