dsh-code-server-app 0.3.47 → 0.3.49
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/README.en.md +166 -3
- package/README.md +175 -8
- package/assets/extensions/dshcs-editor-bridge/webview/THIRD-PARTY.md +1 -1
- package/lib/client.js +2 -2
- package/lib/dsh-resolve.mjs +106 -13
- package/lib/index.js +6 -7
- package/lib/native.js +28 -1
- package/lib/vendored.json +53 -9
- package/package.json +14 -1
- package/scripts/vendor-repacks.mjs +446 -32
- package/vendor/VENDOR.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-code-server-app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.49",
|
|
4
4
|
"description": "VS Code (from a code-server release) inside DSH: a right-sidebar tab driven by the plugin's own launcher over the in-process VS Code server (lib/launcher.mjs). Since 0.3.0 the bundle also ships an editor bridge (assets/extensions/dshcs-editor-bridge): a read-only channel between the in-tree VS Code extension host and DSH, giving the agent what only the editor knows (unsaved buffers, language-server diagnostics, the active selection) and letting editor gestures drive the session. Since 0.3.22 the ask panel renders that session's new content with DSH's own Markdown renderer (pinned to the deployed DSH UI version); since 0.3.23 it is a full-width dialog with collapsible thinking rows and can answer approval requests in place — the bridge's only non-read-only route (POST /approve, constrained to pending in-process requests and the outcomes allowed-once/rejected; panel-first for 5 minutes, handed back to the official card the moment the panel closes). The tab claims DSH file addresses (dsh-resource://file/**) by file type (setting claimExtensions), so the product's own produced-file chips, delivered-file previews and inline prose mentions open in the workbench. The IDE is a resident surface moved with Element.moveBefore instead of being remounted, so switching sidebar tabs no longer reloads it. Opening the tab switches the right sidebar to fullscreen by default (setting fullscreenOnOpen). Following a workspace switch is lightweight: the workbench re-navigates with the new ?folder= and the IDE process is not restarted (since 0.2.12). Requires a DSH with the right-sidebar services (sidebarRightTabs/sidebarRight, >= 0.1.5-alpha.1); older DSH versions get a single upgrade notice on the settings page and no other UI. Two serving modes: loopback port (default) or same-origin mount on DSH's own webServer (/code-server, protected by ctx.connection.requestRejection). No code-server Node layer, no argon2, no C++ toolchain.",
|
|
5
5
|
"homepage": "https://github.com/jinsiyu/dsh-code-server-app",
|
|
6
6
|
"repository": {
|
|
@@ -142,14 +142,24 @@
|
|
|
142
142
|
"zod": "4.4.3"
|
|
143
143
|
},
|
|
144
144
|
"optionalDependencies": {
|
|
145
|
+
"@jinsiyu/dshcs-kerberos-linux-arm64": "2.1.1-dshcs.1",
|
|
146
|
+
"@jinsiyu/dshcs-kerberos-linux-x64": "2.1.1-dshcs.1",
|
|
145
147
|
"@jinsiyu/dshcs-kerberos-win32-arm64": "2.1.1-dshcs.1",
|
|
146
148
|
"@jinsiyu/dshcs-kerberos-win32-x64": "2.1.1-dshcs.1",
|
|
149
|
+
"@jinsiyu/dshcs-vscode-deviceid-linux-arm64": "0.1.5",
|
|
150
|
+
"@jinsiyu/dshcs-vscode-deviceid-linux-x64": "0.1.5",
|
|
147
151
|
"@jinsiyu/dshcs-vscode-deviceid-win32-arm64": "0.1.5",
|
|
148
152
|
"@jinsiyu/dshcs-vscode-deviceid-win32-x64": "0.1.5",
|
|
153
|
+
"@jinsiyu/dshcs-vscode-native-watchdog-linux-arm64": "1.4.6",
|
|
154
|
+
"@jinsiyu/dshcs-vscode-native-watchdog-linux-x64": "1.4.6",
|
|
149
155
|
"@jinsiyu/dshcs-vscode-native-watchdog-win32-arm64": "1.4.6",
|
|
150
156
|
"@jinsiyu/dshcs-vscode-native-watchdog-win32-x64": "1.4.6",
|
|
157
|
+
"@jinsiyu/dshcs-vscode-spdlog-linux-arm64": "0.15.8",
|
|
158
|
+
"@jinsiyu/dshcs-vscode-spdlog-linux-x64": "0.15.8",
|
|
151
159
|
"@jinsiyu/dshcs-vscode-spdlog-win32-arm64": "0.15.8",
|
|
152
160
|
"@jinsiyu/dshcs-vscode-spdlog-win32-x64": "0.15.8",
|
|
161
|
+
"@jinsiyu/dshcs-vscode-sqlite3-linux-arm64": "5.1.12-vscode",
|
|
162
|
+
"@jinsiyu/dshcs-vscode-sqlite3-linux-x64": "5.1.12-vscode",
|
|
153
163
|
"@jinsiyu/dshcs-vscode-sqlite3-win32-arm64": "5.1.12-vscode",
|
|
154
164
|
"@jinsiyu/dshcs-vscode-sqlite3-win32-x64": "5.1.12-vscode",
|
|
155
165
|
"@jinsiyu/dshcs-vscode-windows-ca-certs-win32-arm64": "0.3.4",
|
|
@@ -178,6 +188,9 @@
|
|
|
178
188
|
"test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
|
|
179
189
|
"test:claim-types": "node scripts/test-claim-types.mjs",
|
|
180
190
|
"test:workspace-switch": "node scripts/test-workspace-switch.mjs",
|
|
191
|
+
"test:workspace-cwd": "node scripts/test-workspace-cwd.mjs",
|
|
192
|
+
"test:client-cwd": "node scripts/test-client-bundle-cwd.mjs",
|
|
193
|
+
"test:dsh-resolve": "node scripts/test-dsh-resolve.mjs",
|
|
181
194
|
"test:bridge-routes": "node scripts/test-bridge-routes.mjs",
|
|
182
195
|
"test:bridge-extension": "node scripts/test-bridge-extension.mjs",
|
|
183
196
|
"test:webview": "node scripts/test-webview-bundle.mjs",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// 不给 --from 时自动准备源树(按 vendor/VENDOR.json 的版本 npm install --ignore-scripts,再在
|
|
20
20
|
// lib/vscode 里解包 + rebuild;耗时且需要工具链,维护者换版本时用)。--pack 生成 repack/tgz/*.tgz。
|
|
21
21
|
import { existsSync, readFileSync, readdirSync, mkdirSync, rmSync, writeFileSync, cpSync } from 'node:fs';
|
|
22
|
-
import { dirname, join, relative, resolve } from 'node:path';
|
|
22
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
import { spawnSync } from 'node:child_process';
|
|
25
25
|
|
|
@@ -47,6 +47,17 @@ const DO_PACK = process.argv.includes('--pack');
|
|
|
47
47
|
// VS Code 树包、平台聚合包与插件 package.json。
|
|
48
48
|
// 适用于「原生包没变、只调整打包结构」的场景(本机已无可分析的完整源树时也用它)。
|
|
49
49
|
const REUSE = process.argv.includes('--reuse');
|
|
50
|
+
// --skip-independent:不重建「平台无关」的重打包包。平台无关的包由**单独一条腿**产出并发布
|
|
51
|
+
// (四个目标内容一致,不能四边都发同一个版本号),平台专属腿重建它们纯属浪费,而且会把一份"在别的宿主上
|
|
52
|
+
// 打出来的同名包"塞进 artifact —— 谁要是从 artifact 手工发布,发出去的就是错的内容。
|
|
53
|
+
const SKIP_INDEPENDENT = process.argv.includes('--skip-independent');
|
|
54
|
+
// --only-independent:只产「平台无关」的重打包包 + VS Code 树包,**完全不碰平台专属包**。
|
|
55
|
+
// 独立那条腿用的就是它:平台无关层是所有平台共用的同一份产物,先于平台专属腿执行、只发一次;
|
|
56
|
+
// 平台专属腿各自带 --skip-independent。两者互为反面,同时给直接报错。
|
|
57
|
+
const ONLY_INDEPENDENT = process.argv.includes('--only-independent');
|
|
58
|
+
if (SKIP_INDEPENDENT && ONLY_INDEPENDENT) {
|
|
59
|
+
throw new Error('--skip-independent 与 --only-independent 不能同时给(一个是不产平台无关包,一个是只产它)');
|
|
60
|
+
}
|
|
50
61
|
const TARGETS = argValues('--target');
|
|
51
62
|
// Copilot 整树排除:620MB、依赖链里还有带脚本的包,且 code-server 里用不到。
|
|
52
63
|
const EXCLUDE = [/^@github\//, /^@vscode\/copilot-api$/];
|
|
@@ -306,10 +317,35 @@ function prepareSourceTree() {
|
|
|
306
317
|
for (const dir of innerDirs) {
|
|
307
318
|
if (!existsSync(join(dir, 'node_modules'))) continue;
|
|
308
319
|
console.log(`[repack] 编译内部依赖原生包: ${relative(pkgRoot, dir)}`);
|
|
320
|
+
// ① 整树 rebuild 一次:覆盖声明里没登记的新包。失败不算错 —— 因为 npm 会在**第一个失败**处中断,
|
|
321
|
+
// 一个包编不出来(例如 Windows 上缺 v145 工具集,或 Linux 上缺某个头文件)就会让它后面的包
|
|
322
|
+
// 再也没轮到自己,于是"一个包失败"级联成"一批包没编出来"。
|
|
309
323
|
try {
|
|
310
324
|
npm(['rebuild'], dir);
|
|
311
325
|
} catch (e) {
|
|
312
|
-
console.warn(`[repack]
|
|
326
|
+
console.warn(`[repack] 整树 rebuild 未全部成功(${e.message})⇒ 改为逐个包重试`);
|
|
327
|
+
}
|
|
328
|
+
// ② 逐个「声明为平台专属、且本平台需要」的模块 rebuild:幂等,失败只影响它自己,并逐条报出来。
|
|
329
|
+
// 这样后面 analyze() 的「有没有 .node」判断才是可靠的,而不是随整树中断位置漂移。
|
|
330
|
+
const policy = readRepackPlatforms();
|
|
331
|
+
const hostKey = `${process.platform}-${process.arch}`;
|
|
332
|
+
const failedRebuild = [];
|
|
333
|
+
for (const [alias, decl] of policy.perModule) {
|
|
334
|
+
if (decl.platform !== true) continue; // 平台无关的包靠 prebuilds,不需要现编
|
|
335
|
+
const wanted = decl.targets ?? policy.allTargets ?? null;
|
|
336
|
+
if (Array.isArray(wanted) && !wanted.includes(hostKey)) continue; // 本平台不需要它
|
|
337
|
+
const pkgDir = join(dir, 'node_modules', ...alias.split('/'));
|
|
338
|
+
if (!existsSync(join(pkgDir, 'package.json'))) continue;
|
|
339
|
+
try {
|
|
340
|
+
npm(['rebuild', alias], dir);
|
|
341
|
+
} catch (e) {
|
|
342
|
+
failedRebuild.push(alias);
|
|
343
|
+
console.warn(` ⚠ ${hostKey}: ${alias} rebuild 失败(${e.message})`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
if (failedRebuild.length > 0) {
|
|
347
|
+
console.warn(` ⚠ ${relative(pkgRoot, dir)}:${failedRebuild.length} 个原生包 rebuild 失败`
|
|
348
|
+
+ `(${failedRebuild.join(', ')})—— 若它们在白名单里却没有 .node,最后的硬闸门会直接判失败`);
|
|
313
349
|
}
|
|
314
350
|
}
|
|
315
351
|
return tree;
|
|
@@ -326,11 +362,26 @@ function peMachine(file) {
|
|
|
326
362
|
} catch { return null; }
|
|
327
363
|
}
|
|
328
364
|
|
|
329
|
-
/**
|
|
365
|
+
/** ELF e_machine(0x3e=x86-64 / 0xb7=AArch64),用于 Linux 目标的静态校验。 */
|
|
366
|
+
function elfMachine(file) {
|
|
367
|
+
try {
|
|
368
|
+
const b = readFileSync(file);
|
|
369
|
+
if (b[0] !== 0x7f || b[1] !== 0x45 || b[2] !== 0x4c || b[3] !== 0x46) return null;
|
|
370
|
+
return `0x${b.readUInt16LE(0x12).toString(16)}`;
|
|
371
|
+
} catch { return null; }
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** ① VS Code 树(**必须平台无关**):vendor/vscode → repack/build/vscode/vscode/。
|
|
330
375
|
* 排除 VS Code 的「内部依赖目录」(lib/vscode/node_modules、lib/vscode/extensions/node_modules)
|
|
331
|
-
* ——
|
|
332
|
-
*
|
|
333
|
-
|
|
376
|
+
* —— 核心原生模块(node-pty / @vscode/sqlite3 / spdlog / kerberos / deviceid / native-watchdog /
|
|
377
|
+
* windows-* / @parcel/watcher …)全在那两个目录里,一律由 `@jinsiyu/dshcs-*-<目标>` 按平台供给;
|
|
378
|
+
* 树里只留 lib/vscode 本体与静态资源(browser 资源、语法/主题、字体、.wasm ⇒ 都跨平台)。
|
|
379
|
+
* 为什么树包不按 OS 区分(与上游 VS Code 相反):上游每个平台发一份 server 包,是因为它把
|
|
380
|
+
* **node 运行时 + 原生模块**塞在里面;本产品把这两件事分别交给宿主 DSH 的 node 与平台专属子包
|
|
381
|
+
* (0.1.37 的 `dshcs-code-server-<平台>-<架构>` 全量树就是这么被取代的,见 docs/desktop-first-install-root-cause.md)。
|
|
382
|
+
* 体积与下载账:一份 52MB / 解包 197MB 供所有平台复用,而不是 4 × 52MB。
|
|
383
|
+
* @param moduleNames - 本次重打包模块的原始名(用于校验核心原生模块没有泄漏进树里) */
|
|
384
|
+
function buildVscodeServerPackage(moduleNames = []) {
|
|
334
385
|
const vendorTree = join(pkgRoot, 'vendor', 'vscode');
|
|
335
386
|
if (!existsSync(join(vendorTree, 'lib', 'vscode', 'out', 'server-main.js'))) {
|
|
336
387
|
throw new Error('缺少 vendor/vscode;先运行 `node scripts/vendor-vscode-server.mjs`');
|
|
@@ -356,6 +407,38 @@ function buildVscodeServerPackage() {
|
|
|
356
407
|
},
|
|
357
408
|
});
|
|
358
409
|
const name = `${SCOPE}/dshcs-vscode-server`;
|
|
410
|
+
// 不变式校验:核心原生模块**不许**出现在树里 —— 一旦泄漏,树就变成平台相关(在 win32 腿上打出来的
|
|
411
|
+
// tgz 会被 Linux 用户装上),而它只发布一次、不按 OS 区分 ⇒ 这种错必须在打包期就炸,不能悄悄发出去。
|
|
412
|
+
// 顺带把树里**其它**平台二进制列出来留痕:那些是可选扩展自带的(js-debug 的 win32 预编译、
|
|
413
|
+
// microsoft-authentication 的 msal 运行时、终端 PSReadLine 的 dll),不参与核心运行。
|
|
414
|
+
const leaked = [];
|
|
415
|
+
const others = [];
|
|
416
|
+
const aliasHints = moduleNames.map((n) => String(n).split('/').pop()).filter(Boolean);
|
|
417
|
+
const scan = (cur) => {
|
|
418
|
+
let entries; try { entries = readdirSync(cur, { withFileTypes: true }); } catch { return; }
|
|
419
|
+
for (const e of entries) {
|
|
420
|
+
const full = join(cur, e.name);
|
|
421
|
+
if (e.isDirectory()) {
|
|
422
|
+
if (e.name === 'node_modules') continue; // 已被排除,不该出现在这里
|
|
423
|
+
scan(full);
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
if (!/\.(node|so|dll|dylib|exe)$/i.test(e.name)) continue;
|
|
427
|
+
const rel = relative(dir, full).replaceAll('\\', '/');
|
|
428
|
+
if (aliasHints.some((h) => e.name.includes(h))) leaked.push(rel);
|
|
429
|
+
else others.push(rel);
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
scan(join(dir, 'vscode'));
|
|
433
|
+
if (leaked.length > 0) {
|
|
434
|
+
throw new Error(`${name}: 树里出现了核心原生模块的二进制,树包就不再平台无关了:${leaked.join(', ')}`
|
|
435
|
+
+ ' —— 它们必须留在「内部依赖目录」(lib/vscode/node_modules 或 extensions/node_modules)里,'
|
|
436
|
+
+ '由平台专属子包按目标供给;请检查 vendor/vscode 的结构变化与 buildVscodeServerPackage 的排除规则');
|
|
437
|
+
}
|
|
438
|
+
if (others.length > 0) {
|
|
439
|
+
console.log(`[repack] 树里另有 ${others.length} 个可选扩展自带的平台二进制(不参与核心运行,不影响"一份树包供所有平台"):`);
|
|
440
|
+
for (const o of others) console.log(` · ${o}`);
|
|
441
|
+
}
|
|
359
442
|
writeFileSync(join(dir, 'package.json'), JSON.stringify({
|
|
360
443
|
name,
|
|
361
444
|
version,
|
|
@@ -499,9 +582,177 @@ function readModules(targets) {
|
|
|
499
582
|
return modules;
|
|
500
583
|
}
|
|
501
584
|
|
|
585
|
+
/** 重打包模块的平台政策(scripts/repack-platforms.json,**人工评审**;本脚本只读不写)。
|
|
586
|
+
* @returns {{allTargets:string[]|null, publishedTargets:string[]|null,
|
|
587
|
+
* perModule:Map<string,{platform:boolean|null, targets:string[]|null}>}} */
|
|
588
|
+
function readRepackPlatforms() {
|
|
589
|
+
const doc = readJson(join(here, 'repack-platforms.json'));
|
|
590
|
+
const list = (v) => (Array.isArray(v) ? v.filter((t) => typeof t === 'string' && t !== '') : null);
|
|
591
|
+
const allTargets = list(doc?.targets);
|
|
592
|
+
if (allTargets === null || allTargets.length === 0) {
|
|
593
|
+
console.warn(' ⚠ 读不到 scripts/repack-platforms.json 的 targets ⇒ 平台政策退化为「构建目标即全部目标」,'
|
|
594
|
+
+ '每模块白名单与跨平台保留都会失效');
|
|
595
|
+
}
|
|
596
|
+
const perModule = new Map();
|
|
597
|
+
for (const [alias, spec] of Object.entries(doc?.modules ?? {})) {
|
|
598
|
+
if (spec === null || typeof spec !== 'object') continue;
|
|
599
|
+
perModule.set(alias, {
|
|
600
|
+
platform: typeof spec.platform === 'boolean' ? spec.platform : null,
|
|
601
|
+
targets: list(spec.targets),
|
|
602
|
+
// 可选:我们自己定义的版本(重打包内容变了、而上游版本没变时用它,例如 2.1.1-dshcs.2)。
|
|
603
|
+
version: typeof spec.version === 'string' && spec.version !== '' ? spec.version : null,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
return { allTargets, publishedTargets: list(doc?.publishedTargets), perModule };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/** 平台政策落到分析结果上:
|
|
610
|
+
* ① 分类(是不是平台专属)以声明为准 —— analyze() 的结论随宿主漂移(Linux 上 windows-registry
|
|
611
|
+
* 没有 .node 会被判成平台无关,一旦写进 dependencies,Windows 运行时反而找不到 -win32-* 子包);
|
|
612
|
+
* ② 平台专属模块补上目标白名单(缺席 = 全部目标);
|
|
613
|
+
* ③ **保留本次分析看不到的条目**:用 --target 只构建本机目标时,表里其它平台的模块必须原样留下;
|
|
614
|
+
* ④ 未在声明里登记的模块给出提醒(新模块).
|
|
615
|
+
* @param modules - 就地修改的重打包条目(来自 analyze 或 --reuse) */
|
|
616
|
+
function applyPlatformPolicy(modules, platforms, allTargets) {
|
|
617
|
+
const declared = (alias) => platforms.perModule.get(alias);
|
|
618
|
+
const existing = readJson(join(pkgRoot, 'lib', 'vendored.json'));
|
|
619
|
+
const previous = new Map((Array.isArray(existing?.modules) ? existing.modules : [])
|
|
620
|
+
.filter((e) => e !== null && typeof e === 'object' && typeof e.alias === 'string')
|
|
621
|
+
.map((e) => [e.alias, e]));
|
|
622
|
+
|
|
623
|
+
for (const m of modules) {
|
|
624
|
+
const decl = declared(m.alias);
|
|
625
|
+
if (decl === undefined) {
|
|
626
|
+
console.warn(` ⚠ ${m.alias} 不在 scripts/repack-platforms.json 的模块表里 ⇒ 沿用分析结论`
|
|
627
|
+
+ `(platform=${m.platform});建议登记它,否则换宿主平台时分类会漂移`);
|
|
628
|
+
} else if (decl.platform !== null && decl.platform !== m.platform) {
|
|
629
|
+
console.warn(` ⚠ ${m.alias}:分析结论 platform=${m.platform},声明写的是 platform=${decl.platform} ⇒ 以声明为准`);
|
|
630
|
+
m.platform = decl.platform;
|
|
631
|
+
}
|
|
632
|
+
// 版本政策(**必须宿主与时点都无关**):① 声明里钉的 `version` 优先;② 否则沿用表里**已发布**的
|
|
633
|
+
// 版本;③ 只有表里没有这个模块(新模块)时才采用源树版本。
|
|
634
|
+
// 为什么不能照源树写:表里的版本就是 registry 上我们发布的那个号,而源树的版本会漂 ——
|
|
635
|
+
// · kerberos:源树上游 2.1.1,我们发布的是 2.1.1-dshcs.1(聚合包时代的后缀);
|
|
636
|
+
// · @vscode/proxy-agent:同一个 code-server 版本,维护者当时装到 0.44.0,今天全新装到 0.45.0
|
|
637
|
+
// —— 依赖范围允许漂移,而 0.45.0 这个子包我们根本没发过。
|
|
638
|
+
// 照源树写 ⇒ 换个宿主平台或换一天重跑,插件依赖就会指向不存在的版本,装上直接解析失败。
|
|
639
|
+
// 要让插件升到新上游版本:在 scripts/repack-platforms.json 里给该模块钉 `version`,重跑本脚本
|
|
640
|
+
// 并把新子包发出去(下面这行会说明清楚)。
|
|
641
|
+
const prevVersion = typeof previous.get(m.alias)?.version === 'string' ? previous.get(m.alias).version : null;
|
|
642
|
+
if (decl?.version !== null && decl?.version !== undefined) {
|
|
643
|
+
if (decl.version !== m.version) console.log(` · ${m.alias}:按声明钉版本 ${decl.version}(源树里是 ${m.version})`);
|
|
644
|
+
m.version = decl.version;
|
|
645
|
+
} else if (prevVersion !== null) {
|
|
646
|
+
if (prevVersion !== m.version) {
|
|
647
|
+
console.log(` · ${m.alias}:沿用表里已发布的版本 ${prevVersion}(源树里是 ${m.version});`
|
|
648
|
+
+ `要让插件改用它,请在 scripts/repack-platforms.json 里钉 version=${m.version} 并先发布对应的子包`);
|
|
649
|
+
}
|
|
650
|
+
m.version = prevVersion;
|
|
651
|
+
} else {
|
|
652
|
+
console.log(` · ${m.alias}:新模块 ⇒ 采用源树版本 ${m.version}(记得先发布子包再把它写进依赖表)`);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
const seen = new Set(modules.map((m) => m.alias));
|
|
657
|
+
const kept = [];
|
|
658
|
+
for (const e of [...previous.values()]) {
|
|
659
|
+
if (e === null || typeof e !== 'object' || typeof e.alias !== 'string' || seen.has(e.alias)) continue;
|
|
660
|
+
const decl = declared(e.alias);
|
|
661
|
+
kept.push({
|
|
662
|
+
alias: e.alias,
|
|
663
|
+
package: typeof e.package === 'string' && e.package !== '' ? e.package : `${SCOPE}/dshcs-${flat(e.alias)}`,
|
|
664
|
+
version: typeof e.version === 'string' ? e.version : null,
|
|
665
|
+
platform: decl?.platform ?? e.platform === true,
|
|
666
|
+
targets: decl?.targets ?? (Array.isArray(e.targets) ? e.targets : null),
|
|
667
|
+
carried: true,
|
|
668
|
+
});
|
|
669
|
+
seen.add(e.alias);
|
|
670
|
+
}
|
|
671
|
+
if (kept.length > 0) {
|
|
672
|
+
console.log(`[repack] 保留 ${kept.length} 个本次分析看不到的条目(其它平台的模块):${kept.map((k) => k.alias).join(', ')}`);
|
|
673
|
+
modules.push(...kept);
|
|
674
|
+
modules.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
for (const m of modules) {
|
|
678
|
+
if (!m.platform) continue;
|
|
679
|
+
const wanted = m.targets ?? declared(m.alias)?.targets ?? null;
|
|
680
|
+
m.targets = wanted === null ? [...allTargets] : allTargets.filter((t) => wanted.includes(t));
|
|
681
|
+
if (m.targets.length === 0) {
|
|
682
|
+
console.warn(` ⚠ ${m.alias}(平台专属)在白名单里没有任何目标 ⇒ 不会产出任何子包`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
for (const [alias, decl] of platforms.perModule) {
|
|
686
|
+
if (seen.has(alias) || decl.platform !== true) continue;
|
|
687
|
+
console.warn(` ⚠ 声明里的 ${alias} 既不在本次分析结果、也不在 lib/vendored.json 里 ⇒ 无法为它产出子包`);
|
|
688
|
+
}
|
|
689
|
+
return modules;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/** 平台专属重打包包(写在插件 `optionalDependencies`)自带的**注册表依赖** —— 必须同时提成插件
|
|
693
|
+
* 自己的直接依赖。原因(0.3.46 实测,复现与证据见 docs/desktop-first-install-root-cause.md 第 5 节):
|
|
694
|
+
* pnpm 的增量 hoisted 安装会把「optional 子树里 depth≥2」的依赖整支丢进 `node_modules/.modules.yaml`
|
|
695
|
+
* 的 `skipped`(`bindings` / `fs-extra` / `uuid` / `mkdirp` 全在内),而 dsh-desktop 在 `pnpm add`
|
|
696
|
+
* 之后**立刻**用 `require.resolve.paths()` 校验依赖图 ⇒ 首次安装报
|
|
697
|
+
* `@jinsiyu/dshcs-kerberos-win32-arm64 requires missing bindings@^1.5.0`;
|
|
698
|
+
* 更糟的是 `pnpm install --frozen-lockfile` 会认为 «Already up to date»(锁文件本身也缺这些条目),
|
|
699
|
+
* 于是 profile 永远修不好,只能靠删 node_modules 重装。提到插件根依赖后它们落在 profile 根
|
|
700
|
+
* `node_modules`,校验器与运行时都能解析到。这些都是纯 JS、无 ABI 约束,版本在此钉死;
|
|
701
|
+
* 将来平台专属包新增注册表依赖时 verifyOptionalSubtreeDeps() 会在构建期直接报错。 */
|
|
702
|
+
const OPTIONAL_SUBTREE_DEPS = {
|
|
703
|
+
bindings: '1.5.0',
|
|
704
|
+
mkdirp: '1.0.4',
|
|
705
|
+
'fs-extra': '11.4.0',
|
|
706
|
+
uuid: '14.0.2',
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
/** 构建期闸门:平台专属重打包清单里的每个非别名依赖都必须落在**插件根依赖**里,
|
|
710
|
+
* 否则 pnpm 会把它丢进 skipped。两条来源都算数:
|
|
711
|
+
* · OPTIONAL_SUBTREE_DEPS(仅这些包才需要、由本表钉版本的,例如 bindings);
|
|
712
|
+
* · 树自己的直装集 `declare`(例如 node-addon-api —— VS Code 树本来就把它列为直接依赖,
|
|
713
|
+
* 生成器会按树里的版本写进插件 dependencies,同样落在 profile 根 node_modules)。
|
|
714
|
+
* @param dirs - 已生成的平台专属重打包目录。
|
|
715
|
+
* @param directDeps - 本次将写进插件 dependencies 的名字集合。 */
|
|
716
|
+
function verifyOptionalSubtreeDeps(dirs, directDeps) {
|
|
717
|
+
const seen = new Set();
|
|
718
|
+
for (const dir of dirs) {
|
|
719
|
+
const m = readJson(join(dir, 'package.json'));
|
|
720
|
+
if (m === null) continue;
|
|
721
|
+
for (const fld of ['dependencies', 'optionalDependencies']) {
|
|
722
|
+
for (const [name, spec] of Object.entries(m[fld] ?? {})) {
|
|
723
|
+
if (String(spec).startsWith('npm:')) continue; // 已重打包(真名子包),不走注册表
|
|
724
|
+
seen.add(name);
|
|
725
|
+
if (!Object.hasOwn(OPTIONAL_SUBTREE_DEPS, name) && !directDeps.has(name)) {
|
|
726
|
+
throw new Error(`${m.name}: 注册表依赖 ${name}@${spec} 既不在 OPTIONAL_SUBTREE_DEPS、也不在插件的`
|
|
727
|
+
+ ' 直装依赖里 —— 它位于 optional 子树(depth≥2),pnpm 的增量 hoisted 安装会丢包,'
|
|
728
|
+
+ ' dsh-desktop 安装后立刻校验必报 requires missing;请把它提为插件直接依赖'
|
|
729
|
+
+ '(见 docs/desktop-first-install-root-cause.md 第 5 节)');
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
const unused = Object.keys(OPTIONAL_SUBTREE_DEPS).filter((name) => !seen.has(name));
|
|
735
|
+
if (unused.length > 0) {
|
|
736
|
+
console.warn(` ⚠ OPTIONAL_SUBTREE_DEPS 里的 ${unused.join(', ')} 已不再被任何平台专属包需要,可以删掉`);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
502
740
|
function main() {
|
|
503
741
|
const hostKey = `${process.platform}-${process.arch}`;
|
|
504
|
-
|
|
742
|
+
// 「构建哪些目标」(--target,缺省本机)与「产品覆盖哪些目标」是两件事:前者只影响本次构建,
|
|
743
|
+
// 后者来自 scripts/repack-platforms.json,决定 lib/vendored.json 的 targets、每模块白名单与插件
|
|
744
|
+
// optionalDependencies —— 否则在 Windows 上构建会把 Linux 的条目从依赖表里挤掉(反之亦然)。
|
|
745
|
+
const platforms = readRepackPlatforms();
|
|
746
|
+
const buildTargets = TARGETS.length > 0 ? TARGETS : [hostKey];
|
|
747
|
+
const allTargets = platforms.allTargets ?? buildTargets;
|
|
748
|
+
const publishedTargets = (platforms.publishedTargets ?? buildTargets).filter((t) => allTargets.includes(t));
|
|
749
|
+
for (const t of buildTargets) {
|
|
750
|
+
if (!allTargets.includes(t)) console.warn(` ⚠ --target ${t} 不在 repack-platforms.json 的 targets 里(仍按请求构建)`);
|
|
751
|
+
}
|
|
752
|
+
if (publishedTargets.length === 0) {
|
|
753
|
+
throw new Error('repack-platforms.json 的 publishedTargets 与 targets 没有交集 ⇒ 生成不出插件依赖表');
|
|
754
|
+
}
|
|
755
|
+
console.log(`[repack] 平台政策:产品目标 ${allTargets.join(', ')};已发布(可写进插件依赖)${publishedTargets.join(', ')}`);
|
|
505
756
|
|
|
506
757
|
let repack = null;
|
|
507
758
|
let modules;
|
|
@@ -509,7 +760,7 @@ function main() {
|
|
|
509
760
|
let reusedItems = [];
|
|
510
761
|
let autoSourceTree = null;
|
|
511
762
|
if (REUSE) {
|
|
512
|
-
({ modules, declare, reusedItems } = reuseExisting(
|
|
763
|
+
({ modules, declare, reusedItems } = reuseExisting(buildTargets));
|
|
513
764
|
} else {
|
|
514
765
|
const autoSource = FROM === null;
|
|
515
766
|
const sourceTree = autoSource ? prepareSourceTree() : FROM;
|
|
@@ -525,9 +776,20 @@ function main() {
|
|
|
525
776
|
.sort((a, b) => a.alias.localeCompare(b.alias));
|
|
526
777
|
rmSync(OUT, { recursive: true, force: true });
|
|
527
778
|
}
|
|
779
|
+
applyPlatformPolicy(modules, platforms, allTargets);
|
|
528
780
|
console.log(`\n重打包集(${modules.length}):`);
|
|
529
781
|
for (const m of modules) {
|
|
530
|
-
|
|
782
|
+
const tag = m.platform ? `[平台专属 ${m.targets.join('|')}]` : '[全平台]';
|
|
783
|
+
console.log(` ${m.alias}@${m.version} → ${m.package}${m.platform ? '-<平台>' : ''}${m.carried ? ' (保留)' : ''} ${tag}`);
|
|
784
|
+
}
|
|
785
|
+
const rootPkgFile = join(pkgRoot, 'package.json');
|
|
786
|
+
const previousDeps = readJson(rootPkgFile)?.dependencies ?? {};
|
|
787
|
+
for (const d of declare) {
|
|
788
|
+
const prev = previousDeps[d.name];
|
|
789
|
+
if (typeof prev === 'string' && prev !== d.version) {
|
|
790
|
+
console.log(` · ${d.name}:源树版本 ${d.version}(仓库依赖表里是 ${prev})—— 上游纯 JS 包,跟着树走安全`
|
|
791
|
+
+ '(树里的版本一定来自 npm;这类差异是**时间**相关而非宿主相关)');
|
|
792
|
+
}
|
|
531
793
|
}
|
|
532
794
|
console.log(`\n直装集(${declare.length}): ${declare.map((d) => d.name).join(', ')}\n`);
|
|
533
795
|
|
|
@@ -535,7 +797,7 @@ function main() {
|
|
|
535
797
|
|
|
536
798
|
// 每个目标的包名映射:平台专属的按目标拼后缀,平台无关的到处同名
|
|
537
799
|
const byTarget = new Map();
|
|
538
|
-
for (const target of
|
|
800
|
+
for (const target of buildTargets) {
|
|
539
801
|
const map = new Map(); // 原包名 -> { pkgName, version, platformSpecific }
|
|
540
802
|
for (const m of modules) {
|
|
541
803
|
map.set(m.alias, {
|
|
@@ -588,6 +850,20 @@ function main() {
|
|
|
588
850
|
if (wrong.length > 0) {
|
|
589
851
|
console.warn(` ⚠ ${pkg.pkgName}: ${wrong.length} 个 .node 架构不符(期望 ${want})`);
|
|
590
852
|
}
|
|
853
|
+
} else if (platform === 'linux') {
|
|
854
|
+
// 同上,ELF 版:e_machine 必须是目标架构(0x3e=x86-64 / 0xb7=AArch64)
|
|
855
|
+
const want = arch === 'x64' ? '0x3e' : '0xb7';
|
|
856
|
+
const files = nodeFilesOf(dir);
|
|
857
|
+
const wrong = files
|
|
858
|
+
.map((file) => [file, elfMachine(file)])
|
|
859
|
+
.filter(([, machine]) => machine !== null && machine !== want);
|
|
860
|
+
if (files.length > 0 && wrong.length === files.length) {
|
|
861
|
+
throw new Error(`${pkg.pkgName}: 所有 .node 架构都不是 ${want}`
|
|
862
|
+
+ `(${wrong.map(([file, machine]) => `${relative(dir, file)}=${machine}`).join(', ')})`);
|
|
863
|
+
}
|
|
864
|
+
if (wrong.length > 0) {
|
|
865
|
+
console.warn(` ⚠ ${pkg.pkgName}: ${wrong.length} 个 .node 架构不符(期望 ${want})`);
|
|
866
|
+
}
|
|
591
867
|
}
|
|
592
868
|
}
|
|
593
869
|
for (const fld of ['dependencies', 'optionalDependencies']) {
|
|
@@ -605,44 +881,173 @@ function main() {
|
|
|
605
881
|
// 0) 已打包的原生包(--reuse 模式)
|
|
606
882
|
for (const item of reusedItems) plan.push(item);
|
|
607
883
|
// 1) VS Code 树(平台无关,1 个包)
|
|
608
|
-
const vscodePkg = buildVscodeServerPackage();
|
|
884
|
+
const vscodePkg = buildVscodeServerPackage(modules.map((m) => m.alias));
|
|
609
885
|
plan.push({ dir: vscodePkg.dir, file: vscodePkg.file });
|
|
610
886
|
// 2) 全平台重打包包(从 host 树;--reuse 时用已打包目录)
|
|
887
|
+
// 「要不要按平台分包」一律以**声明**为准(applyPlatformPolicy 已把结论钉到 modules 上),
|
|
888
|
+
// 源树分析只提供源目录与上游版本。照分析结论走的话,本机工具链编不出 .node 时这些模块会被
|
|
889
|
+
// 静默当成平台无关、打成不带目标后缀的包,而依赖表里照旧写着「每目标一份」。
|
|
611
890
|
if (!REUSE) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
891
|
+
const independentModules = modules.filter((m) => !m.platform);
|
|
892
|
+
if (SKIP_INDEPENDENT) {
|
|
893
|
+
console.log(`[repack] --skip-independent:跳过 ${independentModules.length} 个平台无关包`
|
|
894
|
+
+ '(它们由 win32-x64 腿产出并发布,本腿只产自己的平台专属包)');
|
|
895
|
+
} else for (const m of independentModules) {
|
|
896
|
+
const r = repack.get(m.alias);
|
|
897
|
+
if (r === undefined) continue; // 「保留」条目(本宿主看不到它的包)⇒ 没有源目录可打包
|
|
898
|
+
const dir = writeRepack(r.dir, hostMap.get(m.alias), flat(m.alias));
|
|
899
|
+
plan.push({ dir, file: tgzName(hostMap.get(m.alias).pkgName, m.version) });
|
|
616
900
|
}
|
|
617
901
|
}
|
|
618
|
-
// 3) 平台专属包:host 用现成树,其它目标用交叉安装树(--reuse 时用已打包目录)
|
|
619
|
-
|
|
902
|
+
// 3) 平台专属包:host 用现成树,其它目标用交叉安装树(--reuse 时用已打包目录)。
|
|
903
|
+
// · 白名单不含该目标 ⇒ 不构建(Windows-only 模块不产 -linux-*,反之亦然);
|
|
904
|
+
// · 该目标上没编出 .node / 源树里没这个模块 ⇒ 也跳过,但**是否致命交给下面的硬闸门**。
|
|
905
|
+
const platformDirs = [];
|
|
906
|
+
const builtByTarget = new Map(buildTargets.map((t) => [t, []]));
|
|
907
|
+
const gradedSpecific = new Map(buildTargets.map((t) => [t, []]));
|
|
908
|
+
const skipped = { policy: [], noBinary: [], missing: [] };
|
|
909
|
+
if (!REUSE && !ONLY_INDEPENDENT) for (const target of buildTargets) {
|
|
620
910
|
const map = byTarget.get(target);
|
|
621
|
-
const
|
|
911
|
+
const eligible = modules.filter((m) => m.platform === true
|
|
912
|
+
&& (m.targets ?? allTargets).includes(target) && repack.has(m.alias));
|
|
913
|
+
for (const m of modules) {
|
|
914
|
+
if (m.platform === true && repack.has(m.alias) && !(m.targets ?? allTargets).includes(target)) {
|
|
915
|
+
skipped.policy.push(`${m.alias}@${target}`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
gradedSpecific.set(target, eligible.map((m) => m.alias));
|
|
919
|
+
const specs = eligible.map((m) => ({ name: m.alias, version: repack.get(m.alias).pkg.version }));
|
|
622
920
|
let crossTree = null;
|
|
623
921
|
if (target !== hostKey && specs.length > 0) crossTree = prepareCrossTree(specs, target);
|
|
624
|
-
for (const
|
|
625
|
-
|
|
626
|
-
const info = map.get(
|
|
627
|
-
const srcDir = target === hostKey ? r.dir : join(crossTree, 'node_modules',
|
|
922
|
+
for (const m of eligible) {
|
|
923
|
+
const r = repack.get(m.alias);
|
|
924
|
+
const info = map.get(m.alias);
|
|
925
|
+
const srcDir = target === hostKey ? r.dir : join(crossTree, 'node_modules', m.alias);
|
|
628
926
|
if (!existsSync(join(srcDir, 'package.json'))) {
|
|
629
|
-
console.warn(` ⚠ ${target}: 缺少 ${
|
|
927
|
+
console.warn(` ⚠ ${target}: 缺少 ${m.alias},跳过(硬闸门会判定是否致命)`);
|
|
928
|
+
skipped.missing.push(`${m.alias}@${target}`);
|
|
630
929
|
continue;
|
|
631
930
|
}
|
|
632
|
-
|
|
633
|
-
|
|
931
|
+
if (nodeFilesOf(srcDir).length === 0) {
|
|
932
|
+
console.warn(` ⚠ ${target}: ${m.alias} 没有产出 .node 二进制 ⇒ 跳过(硬闸门会判定是否致命)`);
|
|
933
|
+
skipped.noBinary.push(`${m.alias}@${target}`);
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
const dir = writeRepack(srcDir, info, `${flat(m.alias)}-${target}`);
|
|
937
|
+
platformDirs.push(dir);
|
|
938
|
+
builtByTarget.get(target).push(m.alias);
|
|
939
|
+
plan.push({ dir, file: tgzName(info.pkgName, m.version) });
|
|
634
940
|
}
|
|
635
941
|
if (crossTree !== null) rmSync(crossTree, { recursive: true, force: true });
|
|
636
942
|
}
|
|
943
|
+
// 3b) --reuse 时平台专属目录已存在(不重新构建),就地复查闸门
|
|
944
|
+
if (REUSE) for (const item of reusedItems) {
|
|
945
|
+
if (PLATFORM_SUFFIX.test(String(readJson(join(item.dir, 'package.json'))?.name ?? ''))) platformDirs.push(item.dir);
|
|
946
|
+
}
|
|
947
|
+
// 产物 → (目标, 模块) 反查:目录名就是「flat(别名)-目标」。记账(`builtByTarget`)与实际产出的
|
|
948
|
+
// 集合必须**逐个模块**一致 —— 只比个数会漏掉「各 2 个但不是同一批」这种错位
|
|
949
|
+
// (2026-09-16 实测:原生包没编出来时,kerberos 被判成平台无关、目录不带后缀,而 tgz 名却按声明带了
|
|
950
|
+
// -linux-x64 后缀,个数还刚好相等)。
|
|
951
|
+
const packedByTarget = new Map(buildTargets.map((t) => [t, new Set()]));
|
|
952
|
+
for (const dir of platformDirs) {
|
|
953
|
+
const base = basename(dir);
|
|
954
|
+
for (const target of buildTargets) {
|
|
955
|
+
for (const m of modules) {
|
|
956
|
+
if (m.platform === true && base === `${flat(m.alias)}-${target}`) packedByTarget.get(target).add(m.alias);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
// 每个构建目标一行结论(CI 里同时发 annotation:Actions 原始日志要鉴权读不回,
|
|
961
|
+
// annotation 匿名可读,且 ≤20 条上限 —— 所以这里**只发汇总行**,不逐个模块发)。
|
|
962
|
+
if (REUSE) {
|
|
963
|
+
const line = `[repack] --reuse:复用 ${platformDirs.length} 个平台专属目录(不重新构建,故不做白名单/二进制过滤)`;
|
|
964
|
+
console.log(line);
|
|
965
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
|
|
966
|
+
} else if (ONLY_INDEPENDENT) {
|
|
967
|
+
const indep = modules.filter((m) => !m.platform);
|
|
968
|
+
const line = `[repack] --only-independent:产出 ${indep.length} 个平台无关包(${indep.map((m) => m.alias).join(', ')})`
|
|
969
|
+
+ ` + VS Code 树包;平台专属包一律不产(那条腿不跑平台专属循环)`;
|
|
970
|
+
console.log(line);
|
|
971
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
|
|
972
|
+
} else for (const target of buildTargets) {
|
|
973
|
+
const built = builtByTarget.get(target) ?? [];
|
|
974
|
+
const graded = gradedSpecific.get(target) ?? [];
|
|
975
|
+
const packed = packedByTarget.get(target) ?? new Set();
|
|
976
|
+
const n = (list) => list.filter((s) => s.endsWith(`@${target}`)).length;
|
|
977
|
+
const line = `[repack] ${target}: 源树判定平台专属 ${graded.length} 个(${graded.join(', ') || '无'})`
|
|
978
|
+
+ ` ⇒ 打包 ${packed.size} 个(${[...packed].join(', ') || '无'});白名单排除 ${n(skipped.policy)}`
|
|
979
|
+
+ `;无二进制跳过 ${n(skipped.noBinary)}`
|
|
980
|
+
+ `${n(skipped.missing) > 0 ? `;目录缺失跳过 ${n(skipped.missing)}` : ''}`
|
|
981
|
+
+ `;平台无关 ${modules.filter((m) => !m.platform).length} 个`;
|
|
982
|
+
console.log(line);
|
|
983
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::notice::${line}`);
|
|
984
|
+
const onlyBuilt = built.filter((a) => !packed.has(a));
|
|
985
|
+
const onlyPacked = [...packed].filter((a) => !built.includes(a));
|
|
986
|
+
if (onlyBuilt.length > 0 || onlyPacked.length > 0) {
|
|
987
|
+
const detail = `记账与产物不一致:只记账 ${onlyBuilt.join(', ') || '(无)'};只有产物 ${onlyPacked.join(', ') || '(无)'}`;
|
|
988
|
+
console.warn(` ⚠ ${target}: ${detail}`);
|
|
989
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::warning::${target}: ${detail}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (skipped.noBinary.length > 0) {
|
|
993
|
+
console.warn(` ⚠ 以下「模块×目标」在白名单里但没编出二进制:${skipped.noBinary.join(', ')};`
|
|
994
|
+
+ '若该平台确实需要它,查编译日志;若不需要,请从 scripts/repack-platforms.json 里删掉该目标');
|
|
995
|
+
}
|
|
996
|
+
// ── 硬闸门:声明里承诺「某模块在某目标上有子包」,就必须真的产出 ──────────────────────────
|
|
997
|
+
// 为什么必须有它:`npm rebuild` 的失败是被 try/catch 吞掉的(单个原生包编不出来不该卡住整条链),
|
|
998
|
+
// 于是「本机工具链编不出 .node」会静默退化成「这个模块被当成平台无关、打成不带目标后缀的包」,
|
|
999
|
+
// 而依赖表里照旧写着「每目标一份」—— 发出去的就是一套装不起来的东西,而 CI 仍然是绿的。
|
|
1000
|
+
// 2026-09-16 维护者问「为什么 CI 输出里有 npm error 仍然通过了」时暴露:Linux 腿上
|
|
1001
|
+
// @vscode/spdlog / sqlite3 / kerberos 都 npm error,5 个承诺的包只产出 2 个,run 却是绿的。
|
|
1002
|
+
if (ONLY_INDEPENDENT) {
|
|
1003
|
+
// 这条腿只产平台无关层 ⇒ 闸门也只管这一层:声明里(或源树里)的平台无关模块必须都产出。
|
|
1004
|
+
const missing = modules
|
|
1005
|
+
.filter((m) => !m.platform && repack.has(m.alias))
|
|
1006
|
+
.filter((m) => !plan.some((item) => basename(String(item.dir ?? '')) === flat(m.alias)));
|
|
1007
|
+
if (missing.length > 0) {
|
|
1008
|
+
const detail = `--only-independent:这些平台无关重打包包没产出:${missing.map((m) => m.alias).join(', ')}`;
|
|
1009
|
+
console.error(`[repack] ✗ ${detail}`);
|
|
1010
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::error::${detail}`);
|
|
1011
|
+
throw new Error(`${detail} —— 检查源树是否完整(npm install 失败?)和日志里的 npm error`);
|
|
1012
|
+
}
|
|
1013
|
+
} else {
|
|
1014
|
+
const missing = [];
|
|
1015
|
+
for (const target of buildTargets) {
|
|
1016
|
+
const produced = packedByTarget.get(target) ?? new Set();
|
|
1017
|
+
for (const m of modules) {
|
|
1018
|
+
if (m.platform !== true || !(m.targets ?? allTargets).includes(target)) continue;
|
|
1019
|
+
if (!produced.has(m.alias)) missing.push(`${m.alias}@${target}`);
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (missing.length > 0) {
|
|
1023
|
+
const detail = `声明(scripts/repack-platforms.json)里承诺却没产出子包的「模块×目标」:${missing.join(', ')}`;
|
|
1024
|
+
const hint = '常见原因:① 本机工具链编不出该原生包 —— 日志里的 npm error / MSB8020 就是它'
|
|
1025
|
+
+ '(Windows 上注意 MSVC 工具集版本;装对应工具集,或改在 CI 腿上构建);'
|
|
1026
|
+
+ '② 源树里没有这个模块(npm install 失败,或上游把它去掉了);'
|
|
1027
|
+
+ '③ 该目标本来就不该有这个模块 ⇒ 从该模块的 targets 里删掉这个目标。';
|
|
1028
|
+
console.error(`[repack] ✗ ${detail}`);
|
|
1029
|
+
if (process.env.GITHUB_ACTIONS === 'true') console.log(`::error::${detail} —— ${hint}`);
|
|
1030
|
+
throw new Error(`${detail} —— ${hint}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
verifyOptionalSubtreeDeps(platformDirs, new Set([
|
|
1034
|
+
...declare.map((d) => d.name),
|
|
1035
|
+
...Object.keys(OPTIONAL_SUBTREE_DEPS),
|
|
1036
|
+
]));
|
|
637
1037
|
// 3) 重打包表(lib/vendored.json,随插件发布):运行时用它把真名子包补成原始名字
|
|
638
1038
|
const vendoredDoc = {
|
|
639
1039
|
schemaVersion: 1,
|
|
640
1040
|
generatedBy: 'scripts/vendor-repacks.mjs',
|
|
641
1041
|
scope: SCOPE,
|
|
642
|
-
targets,
|
|
1042
|
+
targets: allTargets,
|
|
643
1043
|
modules: modules.map((m) => {
|
|
644
1044
|
const entry = { alias: m.alias, package: m.package, version: m.version };
|
|
645
|
-
if (m.platform)
|
|
1045
|
+
if (m.platform) {
|
|
1046
|
+
entry.platform = true;
|
|
1047
|
+
// 每模块目标白名单:运行时(lib/native.js)据此跳过本平台不适用的模块 ——
|
|
1048
|
+
// 否则 Linux 上会去找 @jinsiyu/dshcs-vscode-windows-registry-linux-x64,把缺包报成故障。
|
|
1049
|
+
entry.targets = [...(Array.isArray(m.targets) ? m.targets : allTargets)];
|
|
1050
|
+
}
|
|
646
1051
|
return entry;
|
|
647
1052
|
}),
|
|
648
1053
|
};
|
|
@@ -655,7 +1060,8 @@ function main() {
|
|
|
655
1060
|
console.log(`[repack] 生成 ${plan.length} 个待打包目录 → repack/ (计划:repack/pack-plan.json)`);
|
|
656
1061
|
|
|
657
1062
|
// 4) 改写插件 package.json 的依赖:树 + 纯 JS 直装集 + **全平台**重打包包写 dependencies(真名);
|
|
658
|
-
// 平台专属的重打包包按目标写 optionalDependencies(真名 + 包自带 os/cpu,包管理器按架构自动选)
|
|
1063
|
+
// 平台专属的重打包包按目标写 optionalDependencies(真名 + 包自带 os/cpu,包管理器按架构自动选),
|
|
1064
|
+
// 它们自己的注册表依赖再由 OPTIONAL_SUBTREE_DEPS 提到 dependencies(否则 pnpm 丢包,见该表注释)。
|
|
659
1065
|
// 这样每个包在依赖图里都是「根项目的直接依赖」,不依赖 pnpm 对可选子树别名的处理
|
|
660
1066
|
// (见 docs/desktop-first-install-root-cause.md)。
|
|
661
1067
|
const pkgFile = join(pkgRoot, 'package.json');
|
|
@@ -664,15 +1070,23 @@ function main() {
|
|
|
664
1070
|
[vscodePkg.name, vscodePkg.version],
|
|
665
1071
|
...declare.map((d) => [d.name, d.version]),
|
|
666
1072
|
...independent.map((m) => [m.package, m.version]),
|
|
1073
|
+
...Object.entries(OPTIONAL_SUBTREE_DEPS),
|
|
667
1074
|
].sort(([a], [b]) => a.localeCompare(b)));
|
|
668
|
-
pluginPkg.optionalDependencies = Object.fromEntries(
|
|
669
|
-
.flatMap((t) => specific.map((m) => [`${m.package}-${t}`, m.version]))
|
|
1075
|
+
pluginPkg.optionalDependencies = Object.fromEntries(publishedTargets
|
|
1076
|
+
.flatMap((t) => specific.filter((m) => m.targets.includes(t)).map((m) => [`${m.package}-${t}`, m.version]))
|
|
670
1077
|
.sort(([a], [b]) => a.localeCompare(b)));
|
|
671
1078
|
writeFileSync(pkgFile, JSON.stringify(pluginPkg, null, 2) + '\n', 'utf8');
|
|
672
1079
|
console.log(`[repack] 已写入 package.json:dependencies ${Object.keys(pluginPkg.dependencies).length} 个`
|
|
673
|
-
+ `(VS Code 树 + 纯 JS + ${independent.length}
|
|
1080
|
+
+ `(VS Code 树 + 纯 JS + ${independent.length} 个全平台重打包包`
|
|
1081
|
+
+ ` + ${Object.keys(OPTIONAL_SUBTREE_DEPS).length} 个 optional 子树兜底依赖),`
|
|
674
1082
|
+ `optionalDependencies ${Object.keys(pluginPkg.optionalDependencies).length} 个`
|
|
675
|
-
+ `(${specific.length} 个平台专属重打包包 × ${
|
|
1083
|
+
+ `(${specific.length} 个平台专属重打包包 × 已发布目标 ${publishedTargets.join('|')},按每模块白名单取交集)`);
|
|
1084
|
+
const pendingTargets = allTargets.filter((t) => !publishedTargets.includes(t));
|
|
1085
|
+
if (pendingTargets.length > 0) {
|
|
1086
|
+
console.log(`[repack] 注意:${pendingTargets.join(', ')} 的子包**还没发布到 npm** ⇒ 暂不写进插件依赖`
|
|
1087
|
+
+ '(写进去会让 pnpm install 解析一个不存在的包)。发布后把 scripts/repack-platforms.json 的'
|
|
1088
|
+
+ ' publishedTargets 补齐、再跑一次本脚本,并刷新 pnpm-lock.yaml');
|
|
1089
|
+
}
|
|
676
1090
|
|
|
677
1091
|
// 5) 可选:直接打包
|
|
678
1092
|
if (DO_PACK) {
|