feat(tools): junction.mjs 跨平台目录链接创建/检测
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
import { symlinkSync, lstatSync, existsSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { platform } from 'node:process';
|
||||
|
||||
// Windows 用 junction(免管理员、仅限目录);其它平台用 dir 符号链接
|
||||
const LINK_TYPE = platform === 'win32' ? 'junction' : 'dir';
|
||||
|
||||
export function isLink(p) {
|
||||
try {
|
||||
return lstatSync(p).isSymbolicLink();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 在 linkPath 建一个指向 targetPath 的目录链接。
|
||||
// 返回 'created' | 'exists';目标不存在或 linkPath 已被真实文件占用则抛错。
|
||||
export function ensureJunction(linkPath, targetPath) {
|
||||
const target = resolve(targetPath); // junction 要求绝对目标
|
||||
if (!existsSync(target)) {
|
||||
throw new Error(`junction target 不存在: ${target}`);
|
||||
}
|
||||
if (isLink(linkPath)) return 'exists';
|
||||
if (existsSync(linkPath)) {
|
||||
throw new Error(`路径已存在且非链接,拒绝覆盖: ${linkPath}`);
|
||||
}
|
||||
symlinkSync(target, linkPath, LINK_TYPE);
|
||||
return 'created';
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { join } from 'node:path';
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { makeTempMonorepo, cleanup } from './helpers.mjs';
|
||||
import { ensureJunction, isLink } from '../lib/junction.mjs';
|
||||
|
||||
test('ensureJunction 创建链接后可经链接读到真源文件', () => {
|
||||
const mono = makeTempMonorepo();
|
||||
try {
|
||||
const link = join(mono.games, 'demo-framework');
|
||||
const status = ensureJunction(link, mono.framework);
|
||||
assert.equal(status, 'created');
|
||||
assert.ok(isLink(link));
|
||||
assert.equal(readFileSync(join(link, 'README.md'), 'utf8'), '# framework src\n');
|
||||
} finally {
|
||||
cleanup(mono.root);
|
||||
}
|
||||
});
|
||||
|
||||
test('ensureJunction 对已存在链接返回 exists 且不抛错', () => {
|
||||
const mono = makeTempMonorepo();
|
||||
try {
|
||||
const link = join(mono.games, 'demo-framework');
|
||||
ensureJunction(link, mono.framework);
|
||||
assert.equal(ensureJunction(link, mono.framework), 'exists');
|
||||
} finally {
|
||||
cleanup(mono.root);
|
||||
}
|
||||
});
|
||||
|
||||
test('ensureJunction 目标不存在时抛错', () => {
|
||||
const mono = makeTempMonorepo();
|
||||
try {
|
||||
assert.throws(() => ensureJunction(join(mono.games, 'x'), join(mono.root, 'no-such')), /target 不存在/);
|
||||
} finally {
|
||||
cleanup(mono.root);
|
||||
}
|
||||
});
|
||||
|
||||
test('ensureJunction 拒绝覆盖已存在的真实目录/文件', () => {
|
||||
const mono = makeTempMonorepo();
|
||||
try {
|
||||
const link = join(mono.games, 'realfile');
|
||||
writeFileSync(link, 'x');
|
||||
assert.throws(() => ensureJunction(link, mono.framework), /非链接/);
|
||||
} finally {
|
||||
cleanup(mono.root);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user