dsh-code-server-app 0.3.46 → 0.3.47

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/lib/serve-dsh.mjs CHANGED
@@ -1,162 +1,162 @@
1
- /**
2
- * lib/serve-dsh.mjs — 把 VS Code server 挂到 DSH 自身 HTTP 端口上的两段式注册(serve=dsh)。
3
- *
4
- * ctx.webServer.register({ kind: 'prefix', path: '/code-server', handler }) ← HTTP
5
- * ctx.webServer.registerUpgrade({ path: '/code-server/<productPath>', handler }) ← WebSocket(精确匹配)
6
- *
7
- * 两条路径都必须先过 ctx.connection.requestRejection():DSH 的 webServer 本身不做任何校验,
8
- * 防护是逐路由的(Host/Origin fence + 浏览器 cookie 认证),缺了它 IDE 会成为源站上唯一的无认证面。
9
- *
10
- * 转发策略(Phase 0 实测):**剥掉挂载前缀**后转给 launcher 的命名管道 —— VS Code 侧保持根挂载,
11
- * 与今天已验证的配置完全一致;客户端渲染出的资源引用全是相对路径,浏览器按 /code-server/ 解析,
12
- * 因此同一套 prefix 路由就能覆盖 HTML、静态资源、vscode-remote-resource 与 /_static/*。
13
- *
14
- * 已知退化:转发端口(/proxy/:port)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里),
15
- * 需要 DSH 上游提供 prefix upgrade;HTTP 转发端口不受影响。
16
- */
17
-
18
- import { request as httpRequest } from 'node:http';
19
- import { connect } from 'node:net';
20
-
21
- export const MOUNT_PATH = '/code-server';
22
-
23
- /** 前缀匹配(与 DSH webServer 的 prefix 语义一致:命中 p 与 p/<anything>)。 */
24
- function matchesPrefix(pathname, prefix) {
25
- return pathname === prefix || pathname.startsWith(`${prefix}/`);
26
- }
27
-
28
- /** 把 /code-server/xxx 转成上游要的 /xxx(空则 '/'),保留 query。 */
29
- function stripPrefix(url, prefix) {
30
- const rest = url.slice(prefix.length);
31
- return rest === '' ? '/' : rest;
32
- }
33
-
34
- function rejectionOf(connection, req) {
35
- try {
36
- return connection.requestRejection({ headers: req.headers }) ?? undefined;
37
- } catch (error) {
38
- return { error };
39
- }
40
- }
41
-
42
- /** 转发目标的两种形态:命名管道(kind:'pipe')或回环 TCP(kind:'tcp')。
43
- * 注意 http.request 用 socketPath、net.connect 用 path —— 两者键名不同,必须在这里翻译。 */
44
- function httpOptions(target, extra) {
45
- return target.kind === 'pipe'
46
- ? { socketPath: target.pipe, ...extra }
47
- : { host: target.host, port: target.port, ...extra };
48
- }
49
-
50
- function connectTo(target, onConnect) {
51
- return target.kind === 'pipe'
52
- ? connect({ path: target.pipe }, onConnect)
53
- : connect({ host: target.host, port: target.port }, onConnect);
54
- }
55
-
56
- /**
57
- * 注册 IDE 的同源挂载点。
58
- * @param {object} options
59
- * @param {object} options.webServer - DSH webServer 服务(register / registerUpgrade)
60
- * @param {object} options.connection - DSH connection 服务(requestRejection)
61
- * @param {() => ({socketPath: string} | {host: string, port: number} | null)} options.getTarget
62
- * 当前 launcher 的转发目标(命名管道为主;loopback TCP 用于测试/回退)
63
- * @param {string} options.productPath - VS Code 客户端路径(quality-commit)
64
- * @param {string} [options.mount] - 挂载前缀,默认 /code-server
65
- * @param {(msg: string) => void} [options.log]
66
- * @returns {{ disposers: (() => void)[], upgradePath: string }}
67
- */
68
- export function mountOnWebServer({ webServer, connection, getTarget, productPath, mount = MOUNT_PATH, log = () => {} }) {
69
- const upgradePath = `${mount}/${productPath}`;
70
- const disposers = [];
71
-
72
- /** 请求被 fence 拒绝时按 DSH 的约定回 401/403(upgrade 走裸 socket)。 */
73
- function fence(req) {
74
- const rejection = rejectionOf(connection, req);
75
- if (rejection === undefined) return null;
76
- if (typeof rejection === 'object' && rejection.error !== undefined) {
77
- log(`requestRejection 抛错,按 403 处理: ${rejection.error?.message ?? rejection.error}`);
78
- return 403;
79
- }
80
- return rejection;
81
- }
82
-
83
- disposers.push(webServer.register({
84
- kind: 'prefix',
85
- path: mount,
86
- handler: (req, res) => {
87
- try {
88
- const pathname = new URL(req.url ?? '/', 'http://x').pathname;
89
- if (!matchesPrefix(pathname, mount)) { res.writeHead(404); res.end(); return; }
90
- const status = fence(req);
91
- if (status !== null) {
92
- res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
93
- res.end(status === 401 ? 'unauthorized\n' : 'forbidden\n');
94
- return;
95
- }
96
- const target = getTarget();
97
- if (target === null) {
98
- res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
99
- res.end('code-server 未启动\n');
100
- return;
101
- }
102
- const upstream = httpRequest(httpOptions(target, {
103
- method: req.method,
104
- path: stripPrefix(req.url ?? '/', mount),
105
- headers: req.headers,
106
- }), (upstreamRes) => {
107
- res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
108
- upstreamRes.pipe(res);
109
- });
110
- upstream.on('error', (error) => {
111
- log(`HTTP 转发失败(${req.method} ${req.url}): ${error.message}`);
112
- if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
113
- if (!res.writableEnded) res.end('code-server 不可达\n');
114
- });
115
- req.on('aborted', () => upstream.destroy());
116
- req.pipe(upstream);
117
- } catch (error) {
118
- // 路由回调里的异常绝不能冒泡到 DSH 的 webServer(会变成未捕获异常)
119
- log(`HTTP 挂载点异常(${req.method} ${req.url}): ${error && error.stack ? error.stack : error}`);
120
- if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
121
- if (!res.writableEnded) res.end('code-server 挂载点异常\n');
122
- }
123
- },
124
- }));
125
-
126
- disposers.push(webServer.registerUpgrade({
127
- path: upgradePath,
128
- handler: (req, socket, head) => {
129
- try {
130
- const status = fence(req);
131
- if (status !== null) {
132
- socket.end(`HTTP/1.1 ${status} ${status === 401 ? 'Unauthorized' : 'Forbidden'}\r\nConnection: close\r\n\r\n`);
133
- return;
134
- }
135
- const target = getTarget();
136
- if (target === null) { socket.destroy(); return; }
137
- const upstream = connectTo(target, () => {
138
- const lines = [`GET ${stripPrefix(req.url ?? '/', mount)} HTTP/1.1`];
139
- for (let i = 0; i < req.rawHeaders.length; i += 2) {
140
- lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
141
- }
142
- upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
143
- if (head !== undefined && head.length > 0) upstream.write(head);
144
- upstream.pipe(socket);
145
- socket.pipe(upstream);
146
- });
147
- upstream.on('error', (error) => { log(`WS 转发失败(${req.url}): ${error.message}`); socket.destroy(); });
148
- socket.on('error', () => upstream.destroy());
149
- socket.on('close', () => upstream.destroy());
150
- } catch (error) {
151
- log(`WS 挂载点异常(${req.url}): ${error && error.stack ? error.stack : error}`);
152
- socket.destroy();
153
- }
154
- },
155
- }));
156
-
157
- log(`已挂载 ${mount}/ (HTTP prefix)+ ${upgradePath} (WS exact)`);
158
- return {
159
- upgradePath,
160
- disposers,
161
- };
162
- }
1
+ /**
2
+ * lib/serve-dsh.mjs — 把 VS Code server 挂到 DSH 自身 HTTP 端口上的两段式注册(serve=dsh)。
3
+ *
4
+ * ctx.webServer.register({ kind: 'prefix', path: '/code-server', handler }) ← HTTP
5
+ * ctx.webServer.registerUpgrade({ path: '/code-server/<productPath>', handler }) ← WebSocket(精确匹配)
6
+ *
7
+ * 两条路径都必须先过 ctx.connection.requestRejection():DSH 的 webServer 本身不做任何校验,
8
+ * 防护是逐路由的(Host/Origin fence + 浏览器 cookie 认证),缺了它 IDE 会成为源站上唯一的无认证面。
9
+ *
10
+ * 转发策略(Phase 0 实测):**剥掉挂载前缀**后转给 launcher 的命名管道 —— VS Code 侧保持根挂载,
11
+ * 与今天已验证的配置完全一致;客户端渲染出的资源引用全是相对路径,浏览器按 /code-server/ 解析,
12
+ * 因此同一套 prefix 路由就能覆盖 HTML、静态资源、vscode-remote-resource 与 /_static/*。
13
+ *
14
+ * 已知退化:转发端口(/proxy/:port)的 WebSocket 无法用精确升级路由覆盖(端口号在路径里),
15
+ * 需要 DSH 上游提供 prefix upgrade;HTTP 转发端口不受影响。
16
+ */
17
+
18
+ import { request as httpRequest } from 'node:http';
19
+ import { connect } from 'node:net';
20
+
21
+ export const MOUNT_PATH = '/code-server';
22
+
23
+ /** 前缀匹配(与 DSH webServer 的 prefix 语义一致:命中 p 与 p/<anything>)。 */
24
+ function matchesPrefix(pathname, prefix) {
25
+ return pathname === prefix || pathname.startsWith(`${prefix}/`);
26
+ }
27
+
28
+ /** 把 /code-server/xxx 转成上游要的 /xxx(空则 '/'),保留 query。 */
29
+ function stripPrefix(url, prefix) {
30
+ const rest = url.slice(prefix.length);
31
+ return rest === '' ? '/' : rest;
32
+ }
33
+
34
+ function rejectionOf(connection, req) {
35
+ try {
36
+ return connection.requestRejection({ headers: req.headers }) ?? undefined;
37
+ } catch (error) {
38
+ return { error };
39
+ }
40
+ }
41
+
42
+ /** 转发目标的两种形态:命名管道(kind:'pipe')或回环 TCP(kind:'tcp')。
43
+ * 注意 http.request 用 socketPath、net.connect 用 path —— 两者键名不同,必须在这里翻译。 */
44
+ function httpOptions(target, extra) {
45
+ return target.kind === 'pipe'
46
+ ? { socketPath: target.pipe, ...extra }
47
+ : { host: target.host, port: target.port, ...extra };
48
+ }
49
+
50
+ function connectTo(target, onConnect) {
51
+ return target.kind === 'pipe'
52
+ ? connect({ path: target.pipe }, onConnect)
53
+ : connect({ host: target.host, port: target.port }, onConnect);
54
+ }
55
+
56
+ /**
57
+ * 注册 IDE 的同源挂载点。
58
+ * @param {object} options
59
+ * @param {object} options.webServer - DSH webServer 服务(register / registerUpgrade)
60
+ * @param {object} options.connection - DSH connection 服务(requestRejection)
61
+ * @param {() => ({socketPath: string} | {host: string, port: number} | null)} options.getTarget
62
+ * 当前 launcher 的转发目标(命名管道为主;loopback TCP 用于测试/回退)
63
+ * @param {string} options.productPath - VS Code 客户端路径(quality-commit)
64
+ * @param {string} [options.mount] - 挂载前缀,默认 /code-server
65
+ * @param {(msg: string) => void} [options.log]
66
+ * @returns {{ disposers: (() => void)[], upgradePath: string }}
67
+ */
68
+ export function mountOnWebServer({ webServer, connection, getTarget, productPath, mount = MOUNT_PATH, log = () => {} }) {
69
+ const upgradePath = `${mount}/${productPath}`;
70
+ const disposers = [];
71
+
72
+ /** 请求被 fence 拒绝时按 DSH 的约定回 401/403(upgrade 走裸 socket)。 */
73
+ function fence(req) {
74
+ const rejection = rejectionOf(connection, req);
75
+ if (rejection === undefined) return null;
76
+ if (typeof rejection === 'object' && rejection.error !== undefined) {
77
+ log(`requestRejection 抛错,按 403 处理: ${rejection.error?.message ?? rejection.error}`);
78
+ return 403;
79
+ }
80
+ return rejection;
81
+ }
82
+
83
+ disposers.push(webServer.register({
84
+ kind: 'prefix',
85
+ path: mount,
86
+ handler: (req, res) => {
87
+ try {
88
+ const pathname = new URL(req.url ?? '/', 'http://x').pathname;
89
+ if (!matchesPrefix(pathname, mount)) { res.writeHead(404); res.end(); return; }
90
+ const status = fence(req);
91
+ if (status !== null) {
92
+ res.writeHead(status, { 'content-type': 'text/plain; charset=utf-8' });
93
+ res.end(status === 401 ? 'unauthorized\n' : 'forbidden\n');
94
+ return;
95
+ }
96
+ const target = getTarget();
97
+ if (target === null) {
98
+ res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8' });
99
+ res.end('code-server 未启动\n');
100
+ return;
101
+ }
102
+ const upstream = httpRequest(httpOptions(target, {
103
+ method: req.method,
104
+ path: stripPrefix(req.url ?? '/', mount),
105
+ headers: req.headers,
106
+ }), (upstreamRes) => {
107
+ res.writeHead(upstreamRes.statusCode ?? 502, upstreamRes.headers);
108
+ upstreamRes.pipe(res);
109
+ });
110
+ upstream.on('error', (error) => {
111
+ log(`HTTP 转发失败(${req.method} ${req.url}): ${error.message}`);
112
+ if (!res.headersSent) res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8' });
113
+ if (!res.writableEnded) res.end('code-server 不可达\n');
114
+ });
115
+ req.on('aborted', () => upstream.destroy());
116
+ req.pipe(upstream);
117
+ } catch (error) {
118
+ // 路由回调里的异常绝不能冒泡到 DSH 的 webServer(会变成未捕获异常)
119
+ log(`HTTP 挂载点异常(${req.method} ${req.url}): ${error && error.stack ? error.stack : error}`);
120
+ if (!res.headersSent) res.writeHead(500, { 'content-type': 'text/plain; charset=utf-8' });
121
+ if (!res.writableEnded) res.end('code-server 挂载点异常\n');
122
+ }
123
+ },
124
+ }));
125
+
126
+ disposers.push(webServer.registerUpgrade({
127
+ path: upgradePath,
128
+ handler: (req, socket, head) => {
129
+ try {
130
+ const status = fence(req);
131
+ if (status !== null) {
132
+ socket.end(`HTTP/1.1 ${status} ${status === 401 ? 'Unauthorized' : 'Forbidden'}\r\nConnection: close\r\n\r\n`);
133
+ return;
134
+ }
135
+ const target = getTarget();
136
+ if (target === null) { socket.destroy(); return; }
137
+ const upstream = connectTo(target, () => {
138
+ const lines = [`GET ${stripPrefix(req.url ?? '/', mount)} HTTP/1.1`];
139
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
140
+ lines.push(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}`);
141
+ }
142
+ upstream.write(`${lines.join('\r\n')}\r\n\r\n`);
143
+ if (head !== undefined && head.length > 0) upstream.write(head);
144
+ upstream.pipe(socket);
145
+ socket.pipe(upstream);
146
+ });
147
+ upstream.on('error', (error) => { log(`WS 转发失败(${req.url}): ${error.message}`); socket.destroy(); });
148
+ socket.on('error', () => upstream.destroy());
149
+ socket.on('close', () => upstream.destroy());
150
+ } catch (error) {
151
+ log(`WS 挂载点异常(${req.url}): ${error && error.stack ? error.stack : error}`);
152
+ socket.destroy();
153
+ }
154
+ },
155
+ }));
156
+
157
+ log(`已挂载 ${mount}/ (HTTP prefix)+ ${upgradePath} (WS exact)`);
158
+ return {
159
+ upgradePath,
160
+ disposers,
161
+ };
162
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-server-app",
3
- "version": "0.3.46",
3
+ "version": "0.3.47",
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": {
@@ -64,34 +64,14 @@
64
64
  "README.md",
65
65
  "README.en.md"
66
66
  ],
67
- "scripts": {
68
- "vendor:vscode": "node scripts/vendor-vscode-server.mjs",
69
- "vendor:latest": "node scripts/vendor-vscode-server.mjs --force",
70
- "vendor:check": "node scripts/vendor-vscode-server.mjs --check",
71
- "vendor:code-server": "node scripts/vendor-code-server.mjs",
72
- "repack:build": "node scripts/vendor-repacks.mjs",
73
- "publish:repacks": "node scripts/publish-repacks.mjs",
74
- "publish:plugin": "node scripts/publish-plugin.mjs",
75
- "promote": "node scripts/promote.mjs",
76
- "prepack": "node scripts/vendor-vscode-server.mjs && node scripts/build-webview.mjs",
77
- "build:client": "node scripts/build-client.mjs",
78
- "build:webview": "node scripts/build-webview.mjs",
79
- "test:apply": "node scripts/test-plugin-apply.mjs",
80
- "test:launcher-routes": "node scripts/test-launcher-routes.mjs",
81
- "test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
82
- "test:claim-types": "node scripts/test-claim-types.mjs",
83
- "test:workspace-switch": "node scripts/test-workspace-switch.mjs",
84
- "test:bridge-routes": "node scripts/test-bridge-routes.mjs",
85
- "test:bridge-extension": "node scripts/test-bridge-extension.mjs",
86
- "test:webview": "node scripts/test-webview-bundle.mjs",
87
- "test:vendored": "node scripts/test-vendored-table.mjs"
88
- },
89
67
  "devDependencies": {
90
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.5-rc.2",
91
- "@deepseek-ai/dsh-client-ui-theme": "0.1.5-rc.2",
68
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.6-alpha.1",
69
+ "@deepseek-ai/dsh-client-ui-theme": "0.1.6-alpha.1",
70
+ "@deepseek-ai/schemastery": "3.18.2",
92
71
  "@shikijs/langs": "4.3.1",
93
72
  "anser": "2.3.5",
94
73
  "clsx": "2.0.0",
74
+ "diff": "9.0.0",
95
75
  "esbuild": "^0.25.0",
96
76
  "katex": "0.16.47",
97
77
  "mdast-util-from-markdown": "2.0.3",
@@ -178,5 +158,29 @@
178
158
  "@jinsiyu/dshcs-vscode-windows-process-tree-win32-x64": "0.8.0",
179
159
  "@jinsiyu/dshcs-vscode-windows-registry-win32-arm64": "1.2.0",
180
160
  "@jinsiyu/dshcs-vscode-windows-registry-win32-x64": "1.2.0"
161
+ },
162
+ "scripts": {
163
+ "test": "node scripts/run-all-tests.mjs",
164
+ "test:all": "node scripts/run-all-tests.mjs",
165
+ "test:installed": "node scripts/test-installed.mjs",
166
+ "vendor:vscode": "node scripts/vendor-vscode-server.mjs",
167
+ "vendor:latest": "node scripts/vendor-vscode-server.mjs --force",
168
+ "vendor:check": "node scripts/vendor-vscode-server.mjs --check",
169
+ "vendor:code-server": "node scripts/vendor-code-server.mjs",
170
+ "repack:build": "node scripts/vendor-repacks.mjs",
171
+ "publish:repacks": "node scripts/publish-repacks.mjs",
172
+ "publish:plugin": "node scripts/publish-plugin.mjs",
173
+ "promote": "node scripts/promote.mjs",
174
+ "build:client": "node scripts/build-client.mjs",
175
+ "build:webview": "node scripts/build-webview.mjs",
176
+ "test:apply": "node scripts/test-plugin-apply.mjs",
177
+ "test:launcher-routes": "node scripts/test-launcher-routes.mjs",
178
+ "test:fullscreen": "node scripts/test-sidebar-fullscreen.mjs",
179
+ "test:claim-types": "node scripts/test-claim-types.mjs",
180
+ "test:workspace-switch": "node scripts/test-workspace-switch.mjs",
181
+ "test:bridge-routes": "node scripts/test-bridge-routes.mjs",
182
+ "test:bridge-extension": "node scripts/test-bridge-extension.mjs",
183
+ "test:webview": "node scripts/test-webview-bundle.mjs",
184
+ "test:vendored": "node scripts/test-vendored-table.mjs"
181
185
  }
182
- }
186
+ }
@@ -283,8 +283,14 @@ function prepareSourceTree() {
283
283
  rmSync(tmp, { recursive: true, force: true });
284
284
  mkdirSync(tmp, { recursive: true });
285
285
  console.log(`[repack] 准备完整源树 code-server@${vendorVersion} → ${tmp}`);
286
+ // **必须显式 --prefix**:不给它时 npm 会往上找"最近的 package.json"当项目根 —— 而 tmp 就在
287
+ // 仓库目录里面(<pkgRoot>/.vendor-tmp/repack-src-<pid>),于是包被装进 <pkgRoot>/node_modules,
288
+ // 而下面按 <tmp>/node_modules/code-server 去找 ⇒ readJson() 拿到 null ⇒ analyze() 读
289
+ // `.dependencies` 时直接 TypeError。2026-09-16 在 ubuntu-latest 上实测到:日志里连
290
+ // 「解包内部依赖」都没有(innerDirs 是空的),因为树根本不在那儿。vendor-vscode-server.mjs
291
+ // 的同一步带了 --prefix,所以那条路一直是对的 —— 这里补齐。
286
292
  npm(['install', `code-server@${vendorVersion}`, '--ignore-scripts', '--omit=dev',
287
- '--no-audit', '--no-fund', '--no-save'], tmp);
293
+ '--no-audit', '--no-fund', '--no-save', '--prefix', tmp], tmp);
288
294
  const tree = join(tmp, 'node_modules', 'code-server');
289
295
  const innerDirs = [join('lib', 'vscode'), join('lib', 'vscode', 'extensions')]
290
296
  .map((rel) => join(tree, rel))
@@ -493,49 +499,6 @@ function readModules(targets) {
493
499
  return modules;
494
500
  }
495
501
 
496
- /** 平台专属重打包包(写在插件 `optionalDependencies`)自带的**注册表依赖** —— 必须同时提成插件
497
- * 自己的直接依赖。原因(0.3.46 实测,复现与证据见 docs/desktop-first-install-root-cause.md 第 5 节):
498
- * pnpm 的增量 hoisted 安装会把「optional 子树里 depth≥2」的依赖整支丢进 `node_modules/.modules.yaml`
499
- * 的 `skipped`(`bindings` / `fs-extra` / `uuid` / `mkdirp` 全在内),而 dsh-desktop 在 `pnpm add`
500
- * 之后**立刻**用 `require.resolve.paths()` 校验依赖图 ⇒ 首次安装报
501
- * `@jinsiyu/dshcs-kerberos-win32-arm64 requires missing bindings@^1.5.0`;
502
- * 更糟的是 `pnpm install --frozen-lockfile` 会认为 «Already up to date»(锁文件本身也缺这些条目),
503
- * 于是 profile 永远修不好,只能靠删 node_modules 重装。提到插件根依赖后它们落在 profile 根
504
- * `node_modules`,校验器与运行时都能解析到。这些都是纯 JS、无 ABI 约束,版本在此钉死;
505
- * 将来平台专属包新增注册表依赖时 verifyOptionalSubtreeDeps() 会在构建期直接报错。 */
506
- const OPTIONAL_SUBTREE_DEPS = {
507
- bindings: '1.5.0',
508
- mkdirp: '1.0.4',
509
- 'fs-extra': '11.4.0',
510
- uuid: '14.0.2',
511
- };
512
-
513
- /** 构建期闸门:平台专属重打包清单里的每个非别名依赖都必须在 OPTIONAL_SUBTREE_DEPS 里。
514
- * @param dirs - 已生成的平台专属重打包目录。 */
515
- function verifyOptionalSubtreeDeps(dirs) {
516
- const seen = new Set();
517
- for (const dir of dirs) {
518
- const m = readJson(join(dir, 'package.json'));
519
- if (m === null) continue;
520
- for (const fld of ['dependencies', 'optionalDependencies']) {
521
- for (const [name, spec] of Object.entries(m[fld] ?? {})) {
522
- if (String(spec).startsWith('npm:')) continue; // 已重打包(真名子包),不走注册表
523
- seen.add(name);
524
- if (!Object.hasOwn(OPTIONAL_SUBTREE_DEPS, name)) {
525
- throw new Error(`${m.name}: 注册表依赖 ${name}@${spec} 不在 OPTIONAL_SUBTREE_DEPS 里 ——`
526
- + ' 它位于 optional 子树(depth≥2),pnpm 的增量 hoisted 安装会丢包,'
527
- + ' dsh-desktop 安装后立刻校验必报 requires missing;请把它提为插件直接依赖'
528
- + '(见 docs/desktop-first-install-root-cause.md 第 5 节)');
529
- }
530
- }
531
- }
532
- }
533
- const unused = Object.keys(OPTIONAL_SUBTREE_DEPS).filter((name) => !seen.has(name));
534
- if (unused.length > 0) {
535
- console.warn(` ⚠ OPTIONAL_SUBTREE_DEPS 里的 ${unused.join(', ')} 已不再被任何平台专属包需要,可以删掉`);
536
- }
537
- }
538
-
539
502
  function main() {
540
503
  const hostKey = `${process.platform}-${process.arch}`;
541
504
  const targets = TARGETS.length > 0 ? TARGETS : [hostKey];
@@ -653,7 +616,6 @@ function main() {
653
616
  }
654
617
  }
655
618
  // 3) 平台专属包:host 用现成树,其它目标用交叉安装树(--reuse 时用已打包目录)
656
- const platformDirs = [];
657
619
  if (!REUSE) for (const target of targets) {
658
620
  const map = byTarget.get(target);
659
621
  const specs = [...repack].filter(([, r]) => r.platformSpecific).map(([name, r]) => ({ name, version: r.pkg.version }));
@@ -668,16 +630,10 @@ function main() {
668
630
  continue;
669
631
  }
670
632
  const dir = writeRepack(srcDir, info, `${flat(name)}-${target}`);
671
- platformDirs.push(dir);
672
633
  plan.push({ dir, file: tgzName(info.pkgName, r.pkg.version) });
673
634
  }
674
635
  if (crossTree !== null) rmSync(crossTree, { recursive: true, force: true });
675
636
  }
676
- // 3b) --reuse 时平台专属目录已存在,就地复查闸门
677
- if (REUSE) for (const item of reusedItems) {
678
- if (PLATFORM_SUFFIX.test(String(readJson(join(item.dir, 'package.json'))?.name ?? ''))) platformDirs.push(item.dir);
679
- }
680
- verifyOptionalSubtreeDeps(platformDirs);
681
637
  // 3) 重打包表(lib/vendored.json,随插件发布):运行时用它把真名子包补成原始名字
682
638
  const vendoredDoc = {
683
639
  schemaVersion: 1,
@@ -699,8 +655,7 @@ function main() {
699
655
  console.log(`[repack] 生成 ${plan.length} 个待打包目录 → repack/ (计划:repack/pack-plan.json)`);
700
656
 
701
657
  // 4) 改写插件 package.json 的依赖:树 + 纯 JS 直装集 + **全平台**重打包包写 dependencies(真名);
702
- // 平台专属的重打包包按目标写 optionalDependencies(真名 + 包自带 os/cpu,包管理器按架构自动选),
703
- // 它们自己的注册表依赖再由 OPTIONAL_SUBTREE_DEPS 提到 dependencies(否则 pnpm 丢包,见该表注释)。
658
+ // 平台专属的重打包包按目标写 optionalDependencies(真名 + 包自带 os/cpu,包管理器按架构自动选)。
704
659
  // 这样每个包在依赖图里都是「根项目的直接依赖」,不依赖 pnpm 对可选子树别名的处理
705
660
  // (见 docs/desktop-first-install-root-cause.md)。
706
661
  const pkgFile = join(pkgRoot, 'package.json');
@@ -709,15 +664,13 @@ function main() {
709
664
  [vscodePkg.name, vscodePkg.version],
710
665
  ...declare.map((d) => [d.name, d.version]),
711
666
  ...independent.map((m) => [m.package, m.version]),
712
- ...Object.entries(OPTIONAL_SUBTREE_DEPS),
713
667
  ].sort(([a], [b]) => a.localeCompare(b)));
714
668
  pluginPkg.optionalDependencies = Object.fromEntries(targets
715
669
  .flatMap((t) => specific.map((m) => [`${m.package}-${t}`, m.version]))
716
670
  .sort(([a], [b]) => a.localeCompare(b)));
717
671
  writeFileSync(pkgFile, JSON.stringify(pluginPkg, null, 2) + '\n', 'utf8');
718
672
  console.log(`[repack] 已写入 package.json:dependencies ${Object.keys(pluginPkg.dependencies).length} 个`
719
- + `(VS Code 树 + 纯 JS + ${independent.length} 个全平台重打包包`
720
- + ` + ${Object.keys(OPTIONAL_SUBTREE_DEPS).length} 个 optional 子树兜底依赖),`
673
+ + `(VS Code 树 + 纯 JS + ${independent.length} 个全平台重打包包),`
721
674
  + `optionalDependencies ${Object.keys(pluginPkg.optionalDependencies).length} 个`
722
675
  + `(${specific.length} 个平台专属重打包包 × ${targets.length} 个目标)`);
723
676
 
@@ -3,10 +3,10 @@
3
3
  "vscodeVersion": "1.137.0",
4
4
  "productPath": "stable-b11dabdaca0d3369986975be285db92c8795cea5",
5
5
  "layout": "vscode-only",
6
- "preparedAt": "2026-09-13T13:24:33.857Z",
6
+ "preparedAt": "2026-09-16T15:37:21.154Z",
7
7
  "source": "registry",
8
- "node": "v24.21.0",
9
- "platform": "win32",
10
- "arch": "arm64",
8
+ "node": "v24.20.0",
9
+ "platform": "linux",
10
+ "arch": "x64",
11
11
  "sizeMB": 197.9
12
12
  }