Files
apes-sidebar/src/client/paths.ts
T
Menghuan1918 857646ea66 refactor(client): 收敛 /sidebar envelope 解析与取扩展名双实现
- api.ts:call 与 fetchUpload 里逐字相同的响应 envelope 解析
  (json().catch(null) + ok/value 判断 + SidebarApiError 构造)收敛为
  私有 readEnvelope<T>(),两处调用;判断顺序与错误码回退逻辑不变。
- 取扩展名双实现收敛: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 不变)。
2026-09-03 12:05:29 +08:00

86 lines
3.7 KiB
TypeScript

/**
* Path projection helpers shared by the explorer rows: a path relative to
* the session cwd (for the @-reference button and "copy relative path").
* The fs-tree joins with '/' even on Windows, so both separators normalize
* to '/' before comparison.
*
* This module is dependency-free (no node:path in the client bundle): the
* host is the authority for path semantics, so this mirror deliberately
* accepts a SUPERSET of absolute forms — anything a Windows host would emit
* (drive letters, UNC) plus POSIX roots. A form the host would reject
* (e.g. a backslash UNC path on a POSIX host) passes through here and then
* fails loudly in the host's requireAbsolute instead of being silently
* joined onto the cwd.
*/
/**
* Mirror of the host's absolute-path notion (see fs-tree.requireAbsolute):
* POSIX roots, Windows drive letters, and Windows UNC network shares in
* both backslash (`\\server\share\...`) and forward-slash
* (`//server/share/...`) form. Deliberately a superset — see the module
* comment — so a produced UNC path is never joined onto the cwd.
*/
export function isAbsolutePath(path: string): boolean {
return path.startsWith('/') || /^[A-Za-z]:[\\/]/.test(path) || /^[\\/]{2}[^\\/]/.test(path)
}
/**
* The path relative to the session's working directory.
* @param cwd - the explorer root (absolute).
* @param path - an absolute entry path from the fs-tree.
* @returns the relative path with '/' separators ('.' for the cwd itself),
* or `path` unchanged when it lies outside the cwd.
*
* The prefix test is case-insensitive: Windows paths (and macOS's
* case-insensitive volumes) may arrive with different casing than the cwd
* row, and the containment decision must not depend on it. The returned
* relative text keeps the caller's own casing.
*/
export function relativeTo(cwd: string, path: string): string {
const base = cwd.replace(/[\\/]+$/, '')
const norm = (value: string): string => value.replace(/\\/g, '/')
const nBase = norm(base)
const nPath = norm(path)
if (nPath === nBase) return '.'
if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1)
return path
}
/**
* Whether `target` lies under `base` (or equals it), tolerant of separator
* style and — on Windows-style drive paths — of letter case. A client-side
* mirror of the host's `isWithin` (fs-tree.ts) used to decide whether a
* git-derived path can be opened in the editor (a linked worktree outside
* the session workspace cannot: the host's workspace fence would reject it).
*/
export function isWithinWorkspace(base: string, target: string): boolean {
const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '')
const b = norm(base)
const t = norm(target)
const lb = b.toLowerCase()
const lt = t.toLowerCase()
return lt === lb || lt.startsWith(`${lb}/`)
}
/**
* The last path segment of a '/'- or '\'-separated path (a diff tab title,
* a worktree label). Returns the whole string when no separator is present.
*/
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
}