From 5ef65c0d0561485f321775be0e84a79834a94128 Mon Sep 17 00:00:00 2001 From: Joywayer Date: Sun, 28 Jun 2026 12:07:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(tools):=20junction.mjs=20=E8=B7=A8?= =?UTF-8?q?=E5=B9=B3=E5=8F=B0=E7=9B=AE=E5=BD=95=E9=93=BE=E6=8E=A5=E5=88=9B?= =?UTF-8?q?=E5=BB=BA/=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../scripts/lib/junction.mjs | 29 +++++++++++ .../scripts/test/junction.test.mjs | 50 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 cocoscreator_projects/scripts/lib/junction.mjs create mode 100644 cocoscreator_projects/scripts/test/junction.test.mjs diff --git a/cocoscreator_projects/scripts/lib/junction.mjs b/cocoscreator_projects/scripts/lib/junction.mjs new file mode 100644 index 0000000..23e3130 --- /dev/null +++ b/cocoscreator_projects/scripts/lib/junction.mjs @@ -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'; +} diff --git a/cocoscreator_projects/scripts/test/junction.test.mjs b/cocoscreator_projects/scripts/test/junction.test.mjs new file mode 100644 index 0000000..c8b7885 --- /dev/null +++ b/cocoscreator_projects/scripts/test/junction.test.mjs @@ -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); + } +});