From 857646ea666a8d930a8ffec2f289aa7bebf0e79d Mon Sep 17 00:00:00 2001 From: Menghuan1918 Date: Thu, 3 Sep 2026 12:05:29 +0800 Subject: [PATCH] =?UTF-8?q?refactor(client):=20=E6=94=B6=E6=95=9B=20/sideb?= =?UTF-8?q?ar=20envelope=20=E8=A7=A3=E6=9E=90=E4=B8=8E=E5=8F=96=E6=89=A9?= =?UTF-8?q?=E5=B1=95=E5=90=8D=E5=8F=8C=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - api.ts:call 与 fetchUpload 里逐字相同的响应 envelope 解析 (json().catch(null) + ok/value 判断 + SidebarApiError 构造)收敛为 私有 readEnvelope(),两处调用;判断顺序与错误码回退逻辑不变。 - 取扩展名双实现收敛:extOf 的实现迁至零依赖的 paths.ts(lang.ts 与 service.ts 均可达的基础模块),lang.ts re-export 保持既有导入面 (tests/file-types.spec.ts 从 lang.ts 导入 extOf);service.ts 删除 模块私有的同体 extOfPath,matchFileViewer 改用 extOf。方向说明: service.ts 不能反向 import lang.ts——lang.ts 携带全部 @codemirror/* 依赖且只经 editor 懒加载 chunk 进入浏览器,core bundle 静态引用它 会把 CodeMirror 拉进核心包(构建产物已验证:client.js / client-registry.js 中 0 处 codemirror,client-editor.js 不变)。 --- src/client/api.ts | 39 +++++++++++++++++++++------------------ src/client/lang.ts | 12 +++++------- src/client/paths.ts | 13 +++++++++++++ src/client/service.ts | 11 ++--------- 4 files changed, 41 insertions(+), 34 deletions(-) diff --git a/src/client/api.ts b/src/client/api.ts index 206fafc..e780f28 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -129,6 +129,25 @@ export type TerminalDepsStatus = note?: string } +/** + * Parse one `/sidebar` JSON response envelope into its value. A non-ok + * status, an unparseable body, or any shape other than `{ok: true, value}` + * surfaces as {@link SidebarApiError} carrying the wire code (falling back + * to the HTTP status). Shared by the JSON api route and the raw upload + * route, whose envelopes are identical. + */ +async function readEnvelope(response: Response): Promise { + const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null + = await response.json().catch(() => null) + if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { + throw new SidebarApiError( + parsed?.error?.code ?? 'http', + parsed?.error?.message ?? `HTTP ${response.status}`, + ) + } + return parsed.value as T +} + async function call(method: string, payload: Record, signal?: AbortSignal): Promise { let response: Response try { @@ -141,15 +160,7 @@ async function call(method: string, payload: Record, signal? } catch (error) { throw new SidebarApiError('network', error instanceof Error ? error.message : String(error)) } - const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null - = await response.json().catch(() => null) - if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { - throw new SidebarApiError( - parsed?.error?.code ?? 'http', - parsed?.error?.message ?? `HTTP ${response.status}`, - ) - } - return parsed.value as T + return readEnvelope(response) } /** @@ -180,15 +191,7 @@ async function fetchUpload( if (error instanceof DOMException && error.name === 'AbortError') throw error throw new SidebarApiError('network', error instanceof Error ? error.message : String(error)) } - const parsed: { ok?: boolean; value?: unknown; error?: { code?: string; message?: string } } | null - = await response.json().catch(() => null) - if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) { - throw new SidebarApiError( - parsed?.error?.code ?? 'http', - parsed?.error?.message ?? `HTTP ${response.status}`, - ) - } - return parsed.value as T + return readEnvelope(response) } /** One request's session scope: the conversation id plus its cwd when known. */ diff --git a/src/client/lang.ts b/src/client/lang.ts index af83e23..424ddc0 100644 --- a/src/client/lang.ts +++ b/src/client/lang.ts @@ -48,14 +48,12 @@ import { pascal } from '@codemirror/legacy-modes/mode/pascal' import { vb } from '@codemirror/legacy-modes/mode/vb' import { vhdl } from '@codemirror/legacy-modes/mode/vhdl' import { stex } from '@codemirror/legacy-modes/mode/stex' +import { extOf } from './paths.ts' -/** The lowercased file extension of a path ('' when none). */ -export function extOf(path: string): string { - const at = path.lastIndexOf('.') - if (at === -1) return '' - const base = path.slice(at + 1).toLowerCase() - return base.includes('/') || base.includes('\\') ? '' : base -} +// Re-exported for the established import surface (tests import extOf from +// this module); the implementation lives in the dependency-free paths.ts so +// core-bundle modules can share it without pulling the CodeMirror packages. +export { extOf } /** Language key for an extension, or null for plain text. Pure (tested). */ export function languageKeyForExt(ext: string): string | null { diff --git a/src/client/paths.ts b/src/client/paths.ts index 7712aa1..e43ea10 100644 --- a/src/client/paths.ts +++ b/src/client/paths.ts @@ -70,3 +70,16 @@ export function baseName(path: string): string { const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) return at === -1 ? path : path.slice(at + 1) } + +/** + * The lowercased file extension of a path ('' when none). The dot must sit + * inside the last segment — a dot in a directory name is not an extension. + * Shared by the editor language mapping (lang.ts) and the viewer registry's + * extension matching (service.ts), which both live in the core bundle. + */ +export function extOf(path: string): string { + const at = path.lastIndexOf('.') + if (at === -1) return '' + const base = path.slice(at + 1).toLowerCase() + return base.includes('/') || base.includes('\\') ? '' : base +} diff --git a/src/client/service.ts b/src/client/service.ts index 45562f9..54856ff 100644 --- a/src/client/service.ts +++ b/src/client/service.ts @@ -27,6 +27,7 @@ import { type SidebarSnapshot, type SidebarState, type SidebarStore, type SidebarTab, } from './state.ts' import { isNarrowWidth } from './breakpoints.ts' +import { extOf } from './paths.ts' import type { SessionScope } from './api.ts' import type { SidebarPrefs } from '../prefs-shared.ts' @@ -427,14 +428,6 @@ export interface BetterSidebarService { openFile(scope: SessionScope, path: string, title?: string): void } -/** Extract the lowercase extension without leading dot from a path. */ -function extOfPath(path: string): string { - const at = path.lastIndexOf('.') - if (at === -1) return '' - const base = path.slice(at + 1).toLowerCase() - return base.includes('/') || base.includes('\\') ? '' : base -} - /** The file name of a path (both separators). */ function baseNameOf(path: string): string { const at = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) @@ -571,7 +564,7 @@ export function createBetterSidebarService(store: SidebarStore): BetterSidebarSe const isViewerEnabled = (id: string): boolean => store.getPrefs().viewersEnabled[id] !== false const matchFileViewer = (path: string, head?: Uint8Array): FileViewerDescriptor | undefined => { - const ext = extOfPath(path) + const ext = extOf(path) // Single pass in priority order (descending; stable for equal // priorities — insertion order). Each descriptor gets first refusal in // its own turn: `detect` (when head bytes are available) beats its own