- fs-tree: symlink 目标 stat 从串行改为 32 并发工作池(readdir 流关闭后统一 探测),symlink 密集/网络目标目录不再累加延迟;顺序与分类语义不变 - html-route: 解码不再重建反斜杠 UNC,统一重建为平台中性的 //server/share/... 前斜杠形态(win32/POSIX 均正确 resolve),从而删除 windowsPathStyle 平台信号 与 client isWindowsStylePath 助手;同时解决无 cwd 时正斜杠 UNC 的往返盲点 - 测试/文档同步;移除未使用的 isPosix
99 lines
5.0 KiB
TypeScript
99 lines
5.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { resolve } from 'node:path'
|
|
import { compareEntries, isWithin, parentOf, requireAbsolute, rootLabel } from '../src/fs-tree.ts'
|
|
import { isWin32 } from './platform.ts'
|
|
|
|
describe('fs-tree', () => {
|
|
it('sorts directories first, then names case-insensitively', () => {
|
|
const rows = [
|
|
{ name: 'b.txt', path: '/x/b.txt', isDir: false, hidden: false, isSymlink: false, broken: false },
|
|
{ name: 'A', path: '/x/A', isDir: true, hidden: false, isSymlink: false, broken: false },
|
|
{ name: 'a.txt', path: '/x/a.txt', isDir: false, hidden: false, isSymlink: false, broken: false },
|
|
{ name: '.hidden', path: '/x/.hidden', isDir: false, hidden: true, isSymlink: false, broken: false },
|
|
]
|
|
expect(rows.sort(compareEntries).map(row => row.name)).toEqual(['A', '.hidden', 'a.txt', 'b.txt'])
|
|
})
|
|
|
|
it('derives root labels and parents (POSIX-style)', () => {
|
|
// POSIX-style inputs behave identically on both platforms (win32 parses '/'
|
|
// as a separator), so these assertions are platform-independent.
|
|
expect(rootLabel('/Users/me/code')).toBe('code')
|
|
expect(rootLabel('/')).toBe('/')
|
|
expect(parentOf('/Users/me/code')).toBe('/Users/me')
|
|
expect(parentOf('/')).toBeUndefined()
|
|
})
|
|
|
|
it.skipIf(!isWin32)('derives root labels and parents for Windows drives', () => {
|
|
expect(rootLabel('C:\\')).toBe('C:\\')
|
|
expect(parentOf('C:\\')).toBeUndefined()
|
|
expect(rootLabel('C:\\Users\\me')).toBe('me')
|
|
expect(parentOf('C:\\Users\\me')).toBe('C:\\Users')
|
|
})
|
|
|
|
it('accepts POSIX absolute paths and rejects relative ones', () => {
|
|
// resolve() is platform-native: '/a/b' roots to the current drive on win32.
|
|
expect(requireAbsolute('/a/b')).toBe(resolve('/a/b'))
|
|
expect(() => requireAbsolute('a/b')).toThrow(/not an absolute path/)
|
|
expect(() => requireAbsolute('../a')).toThrow(/not an absolute path/)
|
|
})
|
|
|
|
// Windows-only path semantics — skipped (not silently passing) on POSIX:
|
|
// drive letters and UNC shares are absolute on win32, drive-relative 'C:foo'
|
|
// is not.
|
|
describe.skipIf(!isWin32)('win32 path semantics', () => {
|
|
it('accepts drive letters and normalizes their separators', () => {
|
|
expect(requireAbsolute('C:/proj')).toBe('C:\\proj')
|
|
expect(requireAbsolute('C:\\proj')).toBe('C:\\proj')
|
|
// A drive path resolves against its own drive, never a bare root.
|
|
expect(resolve('/a/b')).toMatch(/^[A-Za-z]:/)
|
|
})
|
|
|
|
it('accepts UNC network shares in both separator styles', () => {
|
|
expect(requireAbsolute('\\\\server\\share\\proj')).toBe('\\\\server\\share\\proj')
|
|
expect(requireAbsolute('//server/share/proj')).toBe('\\\\server\\share\\proj')
|
|
})
|
|
|
|
it('rejects drive-relative paths', () => {
|
|
expect(() => requireAbsolute('C:proj')).toThrow(/not an absolute path/)
|
|
})
|
|
})
|
|
|
|
// POSIX-only path semantics — the reverse branch of the win32 suite: forms
|
|
// the host can never access must be refused loudly instead of mangled.
|
|
describe.skipIf(isWin32)('POSIX path semantics', () => {
|
|
it('rejects Windows drive paths', () => {
|
|
expect(() => requireAbsolute('C:/proj')).toThrow(/not an absolute path/)
|
|
expect(() => requireAbsolute('C:\\proj')).toThrow(/not an absolute path/)
|
|
})
|
|
|
|
it('rejects backslash UNC paths (not absolute on POSIX)', () => {
|
|
expect(() => requireAbsolute('\\\\server\\share\\proj')).toThrow(/not an absolute path/)
|
|
})
|
|
})
|
|
|
|
it('isWithin tolerates separators and (on win32) letter case', () => {
|
|
expect(isWithin('/work/proj', '/work/proj/src/a.ts')).toBe(true)
|
|
expect(isWithin('/work/proj', '/work/proj')).toBe(true)
|
|
expect(isWithin('/work/proj', '/work/proj2/a.ts')).toBe(false)
|
|
expect(isWithin('/work/proj', '/other/a.ts')).toBe(false)
|
|
// Mixed separators normalize on every platform.
|
|
expect(isWithin('C:\\Users\\me', 'C:/Users/me/src/a.ts')).toBe(true)
|
|
// Case sensitivity follows the platform's filesystem semantics (the
|
|
// platform parameter makes both branches assertable on any host).
|
|
expect(isWithin('C:\\Users\\Me', 'c:/users/me/file.png', 'win32')).toBe(true)
|
|
expect(isWithin('/Users/Me', '/users/me/file.png', 'win32')).toBe(true)
|
|
expect(isWithin('/Users/Me', '/users/me/file.png', 'linux')).toBe(false)
|
|
expect(isWithin('/Users/Me', '/users/me/file.png', 'darwin')).toBe(false)
|
|
// Windows drive-root containment.
|
|
expect(isWithin('C:\\', 'C:\\Users\\me\\a.png', 'win32')).toBe(true)
|
|
expect(isWithin('c:\\users', 'C:/USERS/me/b.png', 'win32')).toBe(true)
|
|
// UNC network-share containment: the '//' share prefix must not defeat
|
|
// the prefix test, and a sibling share must stay outside. The platform
|
|
// parameter is injected, so these win32-semantics assertions run on
|
|
// every host without any platform guard.
|
|
expect(isWithin('\\\\server\\share\\proj', '\\\\server\\share\\proj\\src\\a.ts', 'win32')).toBe(true)
|
|
expect(isWithin('\\\\server\\share\\proj', '\\\\server\\share\\proj2\\a.ts', 'win32')).toBe(false)
|
|
expect(isWithin('\\\\server\\share\\proj', '\\\\other\\share\\a.ts', 'win32')).toBe(false)
|
|
})
|
|
})
|