minigame-std
Version:
Cross-platform standard library for WeChat minigame and web browsers with unified APIs for crypto, fs, fetch, storage, and more.
1 lines • 333 kB
Source Map (JSON)
{"version":3,"file":"main.cjs","names":[],"sources":["../src/macros/env.ts","../src/std/internal/constants.ts","../src/std/internal/helpers.ts","../src/std/internal/validations.ts","../src/std/fetch/mina_fetch.ts","../src/std/fetch/mod.ts","../src/std/path/mod.ts","../src/std/utils/resultify.ts","../src/std/fs/mina_fs_shared.ts","../src/std/fs/mina_fs_async.ts","../src/std/fs/web_fs_helpers.ts","../src/std/fs/fs_async.ts","../src/std/fs/mina_fs_sync.ts","../src/std/fs/fs_sync.ts","../src/std/fs/mod.ts","../src/std/audio/web_audio.ts","../src/std/audio/mod.ts","../src/std/clipboard/mina_clipboard.ts","../src/std/clipboard/web_clipboard.ts","../src/std/clipboard/mod.ts","../src/std/codec/mina_utf8.ts","../src/std/codec/mod.ts","../src/std/crypto/hmac/hmac.ts","../src/std/codec/helpers.ts","../src/std/crypto/hmac/web_hmac.ts","../src/std/crypto/hmac/mod.ts","../src/std/crypto/md/md5.ts","../src/std/crypto/md/mod.ts","../src/std/crypto/random/mina_random.ts","../src/std/crypto/random/web_random.ts","../src/std/crypto/random/mod.ts","../src/std/crypto/rsa/mina_rsa.ts","../src/std/crypto/rsa/web_rsa.ts","../src/std/crypto/rsa/mod.ts","../src/std/crypto/sha/sha.ts","../src/std/crypto/sha/web_sha.ts","../src/std/crypto/sha/mod.ts","../src/std/crypto/mod.ts","../src/std/event/mina_event.ts","../src/std/event/web_event.ts","../src/std/event/mod.ts","../src/std/image/mina_image.ts","../src/std/image/web_image.ts","../src/std/image/mod.ts","../src/std/lbs/mina_lbs.ts","../src/std/lbs/web_lbs.ts","../src/std/lbs/mod.ts","../src/std/network/mina_network.ts","../src/std/network/web_network.ts","../src/std/network/mod.ts","../src/std/platform/base.ts","../src/std/platform/user_agent.ts","../src/std/platform/device.ts","../src/std/platform/target.ts","../src/std/performance/mod.ts","../src/std/platform/mod.ts","../src/std/socket/socket_define.ts","../src/std/socket/mina_socket.ts","../src/std/socket/web_socket.ts","../src/std/socket/mod.ts","../src/std/storage/mina_storage.ts","../src/std/storage/web_storage.ts","../src/std/storage/mod.ts","../src/std/video/web_video.ts","../src/std/video/frame-source/mina.ts","../src/std/video/frame-source/web.ts","../src/std/video/frame-source/mod.ts","../src/std/video/mod.ts"],"sourcesContent":["/**\n * @internal\n * 小游戏环境宏模块。\n */\n\n/**\n * 小游戏环境宏。\n *\n * 可通过打包工具在 build 时修改,如 esbuild webpack vite 等。\n */\ndeclare const __MINIGAME_STD_MINA__: boolean;\n\n/**\n * 如果在小游戏环境中返回 true,否则返回 false。\n */\nexport const IS_MINA = __MINIGAME_STD_MINA__;\n","/**\n * @internal\n * 内部常量。\n */\n\nimport { RESULT_VOID, type AsyncVoidResult } from 'happy-rusty';\n\n/**\n * 异步返回 void 的成功结果常量。\n */\nexport const ASYNC_RESULT_VOID: AsyncVoidResult<never> = /*#__PURE__*/ Promise.resolve(RESULT_VOID);","/**\n * @internal\n * 内部辅助函数。\n */\n\nimport type { FetchTask } from '@happy-ts/fetch-t';\nimport { type IOResult } from 'happy-rusty';\n\n/**\n * 将小游戏失败回调的结果转换为 `Error` 类型。\n *\n * 如果是异步 API 的 `fail` 回调返回的结果通常是 `WechatMinigame.GeneralCallbackResult` 或者变体类型,\n * 如果是同步 API throw 的异常通常是一个类似 `Error` 的类型。\n * @param error - 小游戏错误对象。\n * @returns 转换后的 `Error` 对象。\n */\nexport function miniGameFailureToError(error: WechatMinigame.GeneralCallbackResult | Error): Error {\n return error instanceof Error\n ? error\n // NOTE: 有可能 error 是一个长得像 Error 但不是 Error 实例的对象, 例如: \"statSync:fail no such file or directory\"\n : new Error(error.errMsg ?? (error as unknown as { message: string; }).message);\n}\n\n/**\n * 将 BufferSource 转换为 Uint8Array。\n * @param data - 需要转换的 BufferSource。\n * @returns Uint8Array。\n */\nexport function bufferSourceToBytes(data: BufferSource): Uint8Array<ArrayBuffer> {\n if (data instanceof Uint8Array) {\n return data as Uint8Array<ArrayBuffer>;\n }\n\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n\n if (ArrayBuffer.isView(data)) {\n return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);\n }\n\n throw new TypeError('Input argument must be an ArrayBuffer or ArrayBufferView');\n}\n\n/**\n * 将 BufferSource 转换为 ArrayBuffer。\n * @param data - 需要转换的 BufferSource。\n * @returns ArrayBuffer。\n */\nexport function bufferSourceToAb(data: BufferSource): ArrayBuffer {\n if (data instanceof ArrayBuffer) {\n return data;\n }\n\n if (ArrayBuffer.isView(data)) {\n // 可能存在偏移\n return data.byteOffset === 0\n ? data.buffer\n : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);\n }\n\n throw new TypeError('Input argument must be an ArrayBuffer or ArrayBufferView');\n}\n\n/**\n * 创建一个已失败的 FetchTask 对象。\n * @param errResult - 错误结果。\n * @returns 返回一个已完成的失败 FetchTask。\n */\nexport function createFailedFetchTask<T>(errResult: IOResult<unknown>): FetchTask<T> {\n return {\n abort(): void { /* noop */ },\n get aborted(): boolean { return false; },\n get result() { return Promise.resolve(errResult.asErr<T>()); },\n };\n}","/**\n * @internal\n * 断言相关辅助函数。\n */\n\nimport { Err, RESULT_VOID, type VoidIOResult } from 'happy-rusty';\n\n/**\n * 验证传入的值是否为正整数。\n * @param input - 需要验证的值。\n * @param name - 参数名称,用于错误信息。\n * @returns 验证结果,如果不是数字则返回包含 TypeError 的 Err,如果不是正整数则返回包含 Error 的 Err。\n */\nexport function validatePositiveInteger(input: number, name: string): VoidIOResult {\n if (typeof input !== 'number') {\n return Err(new TypeError(`Param '${name}' must be a number but received ${typeof input}`));\n }\n if (input <= 0 || !Number.isInteger(input)) {\n return Err(new Error(`Param '${name}' must be a positive integer but received ${input}`));\n }\n return RESULT_VOID;\n}\n\n/**\n * 验证传入的值是否为字符串。\n * @param str - 需要验证的值。\n * @param name - 参数名称,用于错误信息。\n * @returns 验证结果,如果不是字符串则返回包含 TypeError 的 Err。\n */\nexport function validateString(str: string, name: string): VoidIOResult {\n if (typeof str !== 'string') {\n return Err(new TypeError(`Param '${name}' must be a string but received ${typeof str}`));\n }\n return RESULT_VOID;\n}\n\n/**\n * 验证传入的 URL 是否为 `https` 协议。\n * @param url - 需要验证的 URL 字符串。\n * @returns 验证结果,如果不是 https 协议则返回 Err。\n */\nexport function validateSafeUrl(url: string): VoidIOResult {\n return validateString(url, 'url')\n .andThen(() => {\n if (!url.startsWith('https://')) {\n return Err(new Error(`Param url must start with https:// but received ${url}`));\n }\n return RESULT_VOID;\n });\n}\n\n/**\n * 验证传入的 WebSocket URL 是否为 `wss` 协议。\n * @param socketUrl - 需要验证的 WebSocket URL 字符串。\n * @returns 验证结果,如果不是 wss 协议则返回 Err。\n */\nexport function validateSafeSocketUrl(socketUrl: string): VoidIOResult {\n return validateString(socketUrl, 'socketUrl')\n .andThen(() => {\n if (!socketUrl.startsWith('wss://')) {\n return Err(new Error(`Param socketUrl must start with wss:// but received ${socketUrl}`));\n }\n return RESULT_VOID;\n });\n}","/**\n * @internal\n * 小游戏平台的 HTTP 请求实现。\n */\n\nimport { ABORT_ERROR, FetchError, TIMEOUT_ERROR, type FetchResult, type FetchTask } from '@happy-ts/fetch-t';\nimport { Err, Ok, type IOResult } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { createFailedFetchTask, miniGameFailureToError, validateSafeUrl } from '../internal/mod.ts';\nimport type { MinaFetchInit } from './fetch_defines.ts';\n\n/**\n * 发起一个可中断的 ArrayBuffer 类型响应的网络请求。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为 ArrayBuffer 且请求可中断。\n * @returns 返回一个 ArrayBuffer 类型的 FetchTask。\n */\nexport function minaFetch(url: string, init: MinaFetchInit & {\n responseType: 'arraybuffer';\n}): FetchTask<ArrayBuffer>;\n\n/**\n * 发起一个可中断的 JSON 类型响应的网络请求。\n * @typeParam T - 预期的 JSON 响应数据类型。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为 JSON 且请求可中断。\n * @returns 返回一个 JSON 类型的 FetchTask。\n */\nexport function minaFetch<T>(url: string, init: MinaFetchInit & {\n responseType: 'json';\n}): FetchTask<T>;\n\n/**\n * 发起一个可中断的文本类型响应的网络请求。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为文本且请求可中断。\n * @returns 返回一个文本类型的 FetchTask。\n */\nexport function minaFetch(url: string, init?: MinaFetchInit & {\n responseType: 'text';\n}): FetchTask<string>;\n\n/**\n * 发起一个可中断的网络请求,默认返回文本类型响应。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定请求可中断。\n * @returns {FetchTask<string>} 返回一个文本类型的 FetchTask。\n */\nexport function minaFetch(url: string, init: MinaFetchInit): FetchTask<string>;\n\n/**\n * 发起一个网络请求,根据初始化配置返回对应类型的 FetchTask。\n * @typeParam T - 预期的响应数据类型。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置。\n * @returns 根据配置返回 FetchTask。\n */\nexport function minaFetch<T>(url: string, init?: MinaFetchInit): FetchTask<T> {\n const urlRes = validateSafeUrl(url);\n if (urlRes.isErr()) return createFailedFetchTask(urlRes);\n\n const {\n responseType,\n onChunk,\n ...rest\n } = init ?? {};\n\n let aborted = false;\n\n const future = new Future<IOResult<T>>();\n\n const options: WechatMinigame.RequestOption = {\n ...rest,\n url,\n success(res) {\n const { statusCode } = res;\n\n if (statusCode >= 200 && statusCode < 300) {\n future.resolve(Ok(res.data as T));\n } else {\n future.resolve(Err(new FetchError(res.errMsg, statusCode)));\n }\n },\n fail(err) {\n const error = miniGameFailureToError(err);\n const { errMsg } = err;\n\n if (errMsg.includes('abort')) {\n error.name = ABORT_ERROR;\n } else if (errMsg.includes('timeout')) {\n error.name = TIMEOUT_ERROR;\n }\n\n future.resolve(Err(error));\n },\n };\n\n if (responseType === 'arraybuffer') {\n options.responseType = responseType;\n } else if (responseType === 'json') {\n options.dataType = responseType;\n } else {\n // 默认 responseType 是 text\n options.responseType = responseType;\n // responseType设置为text还不够,否则返回类型还是json\n options.dataType = '其他';\n }\n\n const task = wx.request(options);\n\n if (typeof onChunk === 'function') {\n task.onChunkReceived(res => {\n onChunk(new Uint8Array(res.data));\n });\n }\n\n return {\n abort(): void {\n aborted = true;\n task.abort();\n },\n\n get aborted(): boolean {\n return aborted;\n },\n\n get result(): FetchResult<T> {\n return future.promise;\n },\n };\n}","/**\n * 网络请求模块,提供可中断的 fetch 请求功能,支持 text、JSON、ArrayBuffer 等响应类型。\n * @module fetch\n */\nimport { fetchT as webFetch, type FetchInit, type FetchTask } from '@happy-ts/fetch-t';\nimport { IS_MINA } from '../../macros/env.ts';\nimport { bufferSourceToAb } from '../internal/mod.ts';\nimport type { MinaFetchInit, UnionFetchInit } from './fetch_defines.ts';\nimport { minaFetch } from './mina_fetch.ts';\n\nexport { ABORT_ERROR, FetchError, TIMEOUT_ERROR, type FetchTask } from '@happy-ts/fetch-t';\nexport type { UnionFetchInit } from './fetch_defines.ts';\n\n/**\n * 发起一个可中断的文本类型响应的网络请求。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为文本且请求可中断。\n * @returns 返回一个文本类型的 FetchTask。\n * @since 1.0.0\n * @example\n * ```ts\n * const task = fetchT('https://api.example.com/data', { responseType: 'text' });\n * const result = await task.result;\n * if (result.isOk()) {\n * console.log(result.unwrap()); // 文本内容\n * }\n * // 如需中断请求\n * task.abort();\n * ```\n */\nexport function fetchT(url: string, init: UnionFetchInit & {\n responseType: 'text';\n}): FetchTask<string>;\n\n/**\n * 发起一个可中断的 ArrayBuffer 类型响应的网络请求。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为 ArrayBuffer 且请求可中断。\n * @returns 返回一个 ArrayBuffer 类型的 FetchTask。\n * @since 1.0.0\n * @example\n * ```ts\n * const task = fetchT('https://api.example.com/file', { responseType: 'arraybuffer' });\n * const result = await task.result;\n * if (result.isOk()) {\n * const buffer = result.unwrap();\n * console.log('文件大小:', buffer.byteLength);\n * }\n * ```\n */\nexport function fetchT(url: string, init: UnionFetchInit & {\n responseType: 'arraybuffer';\n}): FetchTask<ArrayBuffer>;\n\n/**\n * 发起一个可中断的 JSON 类型响应的网络请求。\n * @typeParam T - 预期的 JSON 响应数据类型。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定响应类型为 JSON 且请求可中断。\n * @returns 返回一个 JSON 类型的 FetchTask。\n * @since 1.0.0\n * @example\n * ```ts\n * interface User {\n * id: number;\n * name: string;\n * }\n * const task = fetchT<User>('https://api.example.com/user/1', { responseType: 'json' });\n * const result = await task.result;\n * if (result.isOk()) {\n * const user = result.unwrap();\n * console.log(user.name);\n * }\n * ```\n */\nexport function fetchT<T>(url: string, init: UnionFetchInit & {\n responseType: 'json';\n}): FetchTask<T>;\n\n/**\n * 发起一个可中断的网络请求,默认返回文本类型响应。\n * @typeParam T - 预期的响应数据类型。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置,指定请求可中断。\n * @returns FetchTask。\n * @since 1.0.0\n * @example\n * ```ts\n * const task = fetchT('https://api.example.com/data');\n * const result = await task.result;\n * if (result.isOk()) {\n * console.log(result.unwrap());\n * }\n * ```\n */\nexport function fetchT(url: string, init?: UnionFetchInit): FetchTask<string | Response>;\n\n/**\n * 发起一个网络请求,根据初始化配置返回对应类型的 FetchTask。\n * @typeParam T - 预期的响应数据类型。\n * @param url - 请求的 URL 地址。\n * @param init - 请求的初始化配置。\n * @returns FetchTask。\n * @since 1.0.0\n * @example\n * ```ts\n * // 发起 POST 请求\n * const task = fetchT('https://api.example.com/submit', {\n * method: 'POST',\n * headers: { 'Content-Type': 'application/json' },\n * body: JSON.stringify({ key: 'value' }),\n * responseType: 'json',\n * });\n * const result = await task.result;\n * ```\n */\nexport function fetchT<T>(url: string, init?: UnionFetchInit): FetchTask<T> {\n const defaultInit = init ?? {};\n // 默认是 text 类型\n defaultInit.responseType ??= 'text';\n\n if (IS_MINA) {\n // Map body → data, headers → header for mini-game\n const { body, headers, ...rest } = defaultInit;\n if (body != null) {\n // wx.request data only accepts string | IAnyObject | ArrayBuffer\n // BufferSource needs conversion to handle potential byteOffset\n (rest as MinaFetchInit).data = typeof body === 'string' || isPlainObject(body)\n ? body\n : bufferSourceToAb(body as BufferSource);\n }\n if (headers !== undefined) {\n (rest as MinaFetchInit).header = headers;\n }\n return minaFetch(url, rest) as FetchTask<T>;\n }\n\n // Auto-serialize object body for web\n const { body, ...rest } = defaultInit;\n const webInit: FetchInit & { abortable: true; } = { ...rest, body: body as BodyInit | null | undefined, abortable: true };\n\n if (isPlainObject(body)) {\n webInit.body = JSON.stringify(body);\n // Object body is always serialized as JSON\n const headers = new Headers(webInit.headers);\n headers.set('Content-Type', 'application/json');\n webInit.headers = headers;\n }\n\n return webFetch(url, webInit) as FetchTask<T>;\n}\n\n/**\n * 判断值是否为普通对象(非 string、非 BufferSource)。\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value != null\n && typeof value === 'object'\n && !ArrayBuffer.isView(value)\n && !(value instanceof ArrayBuffer);\n}\n","/**\n * POSIX 路径工具模块,提供 basename、dirname、normalize 等常用路径操作。\n * 仅处理 string 路径,不涉及 URL,确保小游戏平台兼容。\n * @module path\n */\n\n// #region Internal Variables\n\nconst CHAR_FORWARD_SLASH = 47;\nconst CHAR_DOT = 46;\n\n// #endregion\n\n/**\n * 路径分隔符,始终为 '/'。\n * @since 2.4.0\n * @example\n * ```ts\n * import { path } from 'minigame-std';\n *\n * console.log(path.SEPARATOR); // '/'\n * ```\n */\nexport const SEPARATOR = '/';\n\n/**\n * 提取路径的最后一个片段(文件名)。\n * @param path - 要处理的路径字符串。\n * @param suffix - 可选的后缀,如果文件名以此结尾则去除。\n * @returns 路径中的文件名部分。\n * @since 2.4.0\n * @example\n * ```ts\n * import { path } from 'minigame-std';\n *\n * path.basename('/usr/local/file.txt'); // 'file.txt'\n * path.basename('/usr/local/file.txt', '.txt'); // 'file'\n * path.basename('/usr/local/'); // 'local'\n * ```\n */\nexport function basename(path: string, suffix?: string): string {\n if (path.length === 0) return '';\n\n // 去掉尾部斜杠\n let end = path.length;\n while (end > 1 && path.charCodeAt(end - 1) === CHAR_FORWARD_SLASH) end--;\n\n // 只有斜杠的路径\n if (end === 1 && path.charCodeAt(0) === CHAR_FORWARD_SLASH) return SEPARATOR;\n\n // 找最后一个斜杠\n let start = 0;\n for (let i = end - 1; i >= 0; i--) {\n if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {\n start = i + 1;\n break;\n }\n }\n\n let base = path.slice(start, end);\n\n // suffix 不能等于整个文件名\n if (suffix && suffix.length < base.length && base.endsWith(suffix)) {\n base = base.slice(0, -suffix.length);\n }\n\n return base;\n}\n\n/**\n * 提取路径的目录部分。\n * @param path - 要处理的路径字符串。\n * @returns 路径中的目录部分。\n * @since 2.4.0\n * @example\n * ```ts\n * import { path } from 'minigame-std';\n *\n * path.dirname('/usr/local/file.txt'); // '/usr/local'\n * path.dirname('/usr/local/'); // '/usr'\n * path.dirname('file.txt'); // '.'\n * path.dirname('/'); // '/'\n * ```\n */\nexport function dirname(path: string): string {\n if (path.length === 0) return '.';\n\n // 去掉尾部斜杠\n let end = path.length;\n while (end > 1 && path.charCodeAt(end - 1) === CHAR_FORWARD_SLASH) end--;\n\n // 找最后一个斜杠\n let lastSlash = -1;\n for (let i = end - 1; i >= 0; i--) {\n if (path.charCodeAt(i) === CHAR_FORWARD_SLASH) {\n lastSlash = i;\n break;\n }\n }\n\n if (lastSlash === -1) return '.';\n if (lastSlash === 0) return SEPARATOR;\n\n // 去掉结果尾部的连续斜杠\n let dirEnd = lastSlash;\n while (dirEnd > 1 && path.charCodeAt(dirEnd - 1) === CHAR_FORWARD_SLASH) dirEnd--;\n\n return path.slice(0, dirEnd);\n}\n\n/**\n * 规范化路径,解析 '.' 和 '..' 片段,合并多余斜杠。\n * @param path - 要规范化的路径字符串。\n * @returns 规范化后的路径。\n * @since 2.4.0\n * @example\n * ```ts\n * import { path } from 'minigame-std';\n *\n * path.normalize('/foo/bar//baz/asdf/quux/..'); // '/foo/bar/baz/asdf'\n * path.normalize('./foo/../bar/baz'); // 'bar/baz'\n * path.normalize('/foo/bar///baz'); // '/foo/bar/baz'\n * ```\n */\nexport function normalize(path: string): string {\n if (path.length === 0) return '.';\n\n const isAbsolute = path.charCodeAt(0) === CHAR_FORWARD_SLASH;\n const trailingSeparator = path.charCodeAt(path.length - 1) === CHAR_FORWARD_SLASH;\n\n // 解析 . 和 .. 片段\n path = normalizeString(path, !isAbsolute);\n\n if (path.length === 0 && !isAbsolute) path = '.';\n if (path.length > 0 && trailingSeparator) path += SEPARATOR;\n if (isAbsolute) return `${SEPARATOR}${path}`;\n\n return path;\n}\n\n// #region Internal Functions\n\n/**\n * 解析路径中的 '.' 和 '..' 片段。\n * 移植自 path-browserify。\n */\nfunction normalizeString(path: string, allowAboveRoot: boolean): string {\n let res = '';\n let lastSegmentLength = 0;\n let lastSlash = -1;\n let dots = 0;\n let code: number;\n\n for (let i = 0; i <= path.length; i++) {\n if (i < path.length) {\n code = path.charCodeAt(i);\n } else {\n code = CHAR_FORWARD_SLASH;\n }\n\n if (code === CHAR_FORWARD_SLASH) {\n if (lastSlash === i - 1 || dots === 1) {\n // NOOP — empty segment or single dot\n } else if (dots === 2) {\n if (\n res.length < 2\n || lastSegmentLength !== 2\n || res.charCodeAt(res.length - 1) !== CHAR_DOT\n || res.charCodeAt(res.length - 2) !== CHAR_DOT\n ) {\n if (res.length > 2) {\n const lastSlashIndex = res.lastIndexOf(SEPARATOR);\n if (lastSlashIndex === -1) {\n res = '';\n lastSegmentLength = 0;\n } else {\n res = res.slice(0, lastSlashIndex);\n lastSegmentLength = res.length - 1 - res.lastIndexOf(SEPARATOR);\n }\n lastSlash = i;\n dots = 0;\n continue;\n } else if (res.length === 2 || res.length === 1) {\n res = '';\n lastSegmentLength = 0;\n lastSlash = i;\n dots = 0;\n continue;\n }\n }\n if (allowAboveRoot) {\n if (res.length > 0) res += `${SEPARATOR}..`;\n else res = '..';\n lastSegmentLength = 2;\n }\n } else {\n if (res.length > 0) res += `${SEPARATOR}${path.slice(lastSlash + 1, i)}`;\n else res = path.slice(lastSlash + 1, i);\n lastSegmentLength = i - lastSlash - 1;\n }\n lastSlash = i;\n dots = 0;\n } else if (code === CHAR_DOT && dots !== -1) {\n dots++;\n } else {\n dots = -1;\n }\n }\n\n return res;\n}\n\n// #endregion\n","import { Err, Ok, tryAsyncResult, type AsyncIOResult, type AsyncResult, type IOResult, type Result } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { miniGameFailureToError } from '../internal/helpers.js';\n\n/**\n * 将小游戏异步 API 转换为返回 `AsyncResult<T, E>` 的新函数,需要转换的 API 必须是接受可选 `success` 和 `fail` 回调的函数,并且其返回值必须是 `void` 或 `PromiseLike`。\n *\n * 其中 `T` 为 `success` 回调的参数类型,`E` 为 `fail` 回调的参数类型。\n *\n * @param api - 小游戏异步 API。\n * @returns 返回一个新的函数,该函数返回 `AsyncResult<T, E>`。\n * @since 2.0.0\n * @example\n * ```ts\n * // 将 wx.setStorage 转换为 AsyncResult 风格\n * const setStorageAsync = asyncResultify(wx.setStorage);\n * const result = await setStorageAsync({ key: 'test', data: 'value' });\n * if (result.isOk()) {\n * console.log('存储成功');\n * } else {\n * console.error('存储失败:', result.unwrapErr());\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 函数泛型约束需要 any 以兼容所有函数签名\nexport function asyncResultify<F extends (...args: any[]) => unknown, T = ResultifySuccessType<F>, E = ResultifyFailType<F>>(api: F): ResultifyValidAPI<F> extends true\n ? (...args: Parameters<F>) => AsyncResult<T, E>\n : never {\n return ((...args: Parameters<F>): AsyncResult<T, E> => {\n const future = new Future<Result<T, E>>();\n\n const options = args[0] ?? {};\n const { success, fail } = options;\n\n // 强制使用callback的方式调用,即使支持Promise\n options.success = (res: T) => {\n // success 回调依旧执行\n success?.(res);\n future.resolve(Ok(res));\n };\n options.fail = (err: E) => {\n // fail 回调依旧执行\n fail?.(err);\n future.resolve(Err(err));\n };\n\n const ret = api(options);\n\n // 也支持其他返回 PromiseLike 的 API(鸭子类型检查)\n if (ret != null && typeof ret === 'object' && typeof (ret as PromiseLike<T>).then === 'function') {\n // Convert PromiseLike to AsyncResult\n return tryAsyncResult(ret as PromiseLike<T>);\n } else if (ret !== undefined) {\n // 实测发现某些小游戏平台的 API 可能会返回非 void 非 PromiseLike 的值,虽然官方文档通常只描述了 void 或 PromiseLike 的返回类型。\n // 为了兼容这些实际情况,我们暂时不抛出错误,而是允许这种情况存在。\n // throw new TypeError('API must return void or PromiseLike. Otherwise the return value will be discarded');\n }\n\n return future.promise;\n }) as ResultifyValidAPI<F> extends true ? (...args: Parameters<F>) => AsyncResult<T, E> : never;\n}\n\n/**\n * `asyncResultify` 的变体,将小游戏异步 API 转换为返回 `AsyncIOResult<T>` 的新函数。\n *\n * 与 `asyncResultify` 不同的是,此函数会将 `fail` 回调的 `WechatMinigame.GeneralCallbackResult` 转换为 `Error` 类型。\n *\n * @param api - 小游戏异步 API。\n * @returns 返回一个新的函数,该函数返回 `AsyncIOResult<T>`。\n * @since 2.0.0\n * @example\n * ```ts\n * // 将 wx.setStorage 转换为 AsyncIOResult 风格\n * const setStorageAsync = asyncIOResultify(wx.setStorage);\n * const result = await setStorageAsync({ key: 'test', data: 'value' });\n * if (result.isOk()) {\n * console.log('存储成功');\n * } else {\n * console.error('存储失败:', result.unwrapErr().message);\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 函数泛型约束需要 any 以兼容所有函数签名\nexport function asyncIOResultify<F extends (...args: any[]) => unknown, T = ResultifySuccessType<F>>(api: F): IOResultifyValidAPI<F> extends true\n ? (...args: Parameters<F>) => AsyncIOResult<T>\n : never {\n const wrapped = asyncResultify<F, T, WechatMinigame.GeneralCallbackResult>(api);\n\n return (async (...args: Parameters<F>): AsyncIOResult<T> => {\n const result = await wrapped(...args);\n return result.mapErr(miniGameFailureToError);\n }) as IOResultifyValidAPI<F> extends true ? (...args: Parameters<F>) => AsyncIOResult<T> : never;\n}\n\n/**\n * 将小游戏同步 API 转换为返回 `IOResult<T>` 的新函数。\n *\n * 功能类似于 `tryGeneralSyncOp`,但以函数包装的方式使用,将可能抛出的异常捕获并转换为 `IOResult`。\n *\n * @param api - 小游戏同步 API。\n * @returns 返回一个新的函数,该函数返回 `IOResult<T>`。\n * @since 2.0.0\n * @example\n * ```ts\n * // 将 wx.getStorageSync 转换为 IOResult 风格\n * const getStorageSync = syncIOResultify(wx.getStorageSync);\n * const result = getStorageSync('test');\n * if (result.isOk()) {\n * console.log('获取成功:', result.unwrap());\n * } else {\n * console.error('获取失败:', result.unwrapErr().message);\n * }\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 函数泛型约束需要 any 以兼容所有函数签名\nexport function syncIOResultify<F extends (...args: any[]) => unknown>(api: F): (...args: Parameters<F>) => IOResult<ReturnType<F>> {\n return (...args: Parameters<F>): IOResult<ReturnType<F>> => {\n try {\n return Ok(api(...args) as ReturnType<F>);\n } catch (e) {\n return Err(miniGameFailureToError(e as WechatMinigame.GeneralCallbackResult));\n }\n };\n}\n\n// #region Internal Types\n\n/**\n * 通用回调函数类型。\n */\ntype AnyCallback = (...args: never[]) => unknown;\n\n/**\n * 类型工具:判断 API 是否符合 resultify 条件。\n *\n * 要求 API 返回 `void` 或 `PromiseLike`,且参数包含 `success` 或 `fail` 回调。\n * @typeParam T - 待检查的 API 函数类型。\n */\ntype ResultifyValidAPI<T> = T extends (params: infer P) => infer R\n ? R extends void | PromiseLike<unknown>\n ? P extends { success?: AnyCallback; } | undefined\n ? true\n : P extends { fail?: AnyCallback; } | undefined\n ? true\n : false\n : false\n : false;\n\n/**\n * 类型工具:判断 API 是否符合 asyncIOResultify 条件。\n *\n * 在 `ResultifyValidAPI` 基础上,额外要求 `fail` 回调参数类型必须精确为 `GeneralCallbackResult`。\n * @typeParam T - 待检查的 API 函数类型。\n */\ntype IOResultifyValidAPI<T> = ResultifyValidAPI<T> extends true\n ? ResultifyFailType<T> extends WechatMinigame.GeneralCallbackResult\n ? WechatMinigame.GeneralCallbackResult extends ResultifyFailType<T>\n ? true\n : false\n : false\n : false;\n\n/**\n * 类型工具:提取成功回调参数类型。\n *\n * 从 API 函数的 `success` 回调中提取返回类型。\n * @typeParam T - API 函数类型。\n */\ntype ResultifySuccessType<T> = T extends (params: infer P) => unknown\n ? P extends { success?: (res: infer S) => unknown; }\n ? S\n : never\n : never;\n\n/**\n * 类型工具:提取失败回调参数类型。\n *\n * 从 API 函数的 `fail` 回调中提取错误类型。\n * @typeParam T - API 函数类型。\n */\ntype ResultifyFailType<T> = T extends (params: infer P) => unknown\n ? P extends { fail?: (err: infer E) => unknown; }\n ? E\n : never\n : never;\n\n// #endregion\n","/**\n * @internal\n * 同步/异步的公共代码。\n */\n\nimport { normalize } from '../path/mod.ts';\nimport { NOT_FOUND_ERROR, NOTHING_TO_ZIP_ERROR, ROOT_DIR, type ExistsOptions } from 'happy-opfs';\nimport { Err, Lazy, Ok, RESULT_FALSE, RESULT_VOID, tryResult, type IOResult, type VoidIOResult } from 'happy-rusty';\nimport { bufferSourceToAb, miniGameFailureToError } from '../internal/mod.ts';\nimport type { ReadOptions, WriteFileContent } from './fs_define.ts';\n\n// #region Internal Variables\n\n/**\n * 小游戏文件系统管理器实例。\n *\n */\nconst fs = /*#__PURE__*/ Lazy(() => wx.getFileSystemManager());\n\n/**\n * 用户数据根目录,`wxfile://usr` 或 `http://usr`。\n *\n */\nconst usrPath = /*#__PURE__*/ Lazy(() => wx.env.USER_DATA_PATH);\n\n/**\n * 根路径,`wxfile://` 或 `http://`。\n *\n */\nconst rootPath = /*#__PURE__*/ Lazy(() => {\n const path = usrPath.force();\n // 剥离 `usr`\n return `${path.split('://')[0]}://`;\n});\n\n// #endregion\n\n/**\n * zip 操作的结果。\n */\nexport type ZipIOResult = IOResult<Uint8Array<ArrayBuffer> | void>;\n\nexport const EMPTY_BYTES: Uint8Array<ArrayBuffer> = /*#__PURE__*/ new Uint8Array(0);\n\n/**\n * 获取小游戏文件系统管理器实例。\n * @returns 文件系统管理器实例。\n */\nexport function getFs(): WechatMinigame.FileSystemManager {\n return fs.force();\n}\n\n/**\n * 获取文件系统的根路径。\n * @returns 文件系统的根路径。\n */\nexport function getUsrPath(): string {\n return usrPath.force();\n}\n\n/**\n * 验证并标准化路径,返回绝对路径。\n *\n * 支持两种输入格式:\n * 1. 完整路径:以 `wxfile://` 或 `http://` 开头(如 `wxfile://usr/test`)\n * 2. 相对路径:以 `/` 开头(如 `/test`),会自动拼接 `wx.env.USER_DATA_PATH`\n *\n * @param path - 待验证的路径。\n * @returns 验证成功返回标准化后的绝对路径,失败返回错误信息。\n */\nexport function validateAbsolutePath(path: string): IOResult<string> {\n const typeError = validatePathType(path);\n if (typeError) return typeError;\n\n // 是否已经是完整路径\n let isFullPath = false;\n // 检查是否为完整路径(以 `wxfile://` 或 `http://` 开头)\n if (path.startsWith(rootPath.force())) {\n isFullPath = true;\n // 先剥离协议前缀,避免 normalize 将 `://` 转换为 `:/`\n path = path.slice(rootPath.force().length);\n // 传根路径没意义\n if (!path) {\n return Err(new Error('Path must not be root directory'));\n }\n }\n\n // 标准化路径(处理 `.`、`..` 等)并去除末尾的 `/`\n const normalized = normalize(path);\n path = normalized.length > 1 && normalized[normalized.length - 1] === ROOT_DIR\n ? normalized.slice(0, -1)\n : normalized;\n\n // 完整路径:重新拼接协议前缀\n if (isFullPath) {\n // 还是根路径\n if (path === ROOT_DIR) {\n return Err(new Error('Path must not be root directory'));\n }\n // 确保路径不是以 `/` 开头,避免 `wxfile:///usr` 这样的多斜杠情况\n if (path[0] === ROOT_DIR) {\n path = path.slice(1);\n }\n return Ok(rootPath.force() + path);\n }\n\n // 相对路径:必须以 `/` 开头\n if (path[0] !== ROOT_DIR) {\n return Err(new Error(`Path must be absolute (start with '/'): '${path}'`));\n }\n\n // 拼接用户数据根目录\n return Ok(usrPath.force() + path);\n}\n\n/**\n * 验证可读路径(用于只读操作:readFile、stat、readDir)。\n *\n * 支持三种输入格式:\n * 1. 完整路径:以 `wxfile://` 或 `http://` 开头(如 `wxfile://usr/test`)\n * 2. 用户数据相对路径:以 `/` 开头(如 `/test`),会自动拼接 `wx.env.USER_DATA_PATH`\n * 3. 代码包路径:不以 `./`、`../` 开头的相对路径(如 `images/logo.png`),直接返回原路径\n *\n * @param path - 待验证的路径。\n * @returns 验证成功返回标准化后的路径,失败返回错误信息。\n */\nexport function validateReadablePath(path: string): IOResult<string> {\n const typeError = validatePathType(path);\n if (typeError) return typeError;\n\n // 对于完整路径和以 / 开头的路径,使用现有的绝对路径验证\n if (path.startsWith(rootPath.force()) || path.startsWith(ROOT_DIR)) {\n return validateAbsolutePath(path);\n }\n\n // 既不是完整路径,也不以 / 开头,检查是否为代码包路径(不以 ./、../ 开头)\n if (path.startsWith('./') || path.startsWith('../')) {\n return Err(new Error(`Invalid path: '${path}'. Code package paths must not start with './' or '../'`));\n }\n\n // 代码包路径返回标准化后的结果\n const normalized = normalize(path);\n return Ok(normalized);\n}\n\n/**\n * 验证提供的 ExistsOptions 是否有效。\n * `isDirectory` 和 `isFile` 不能同时为 `true`。\n *\n * @param options - 要验证的 ExistsOptions。\n * @returns 表示成功或错误的 VoidIOResult。\n */\nexport function validateExistsOptions(options?: ExistsOptions): VoidIOResult {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n return isDirectory && isFile\n ? Err(new Error('isDirectory and isFile cannot both be true'))\n : RESULT_VOID;\n}\n\n/**\n * 判断错误是否为 `NotFoundError`。\n * @param error - 要检查的错误。\n * @returns 如果是 `NotFoundError` 返回 `true`,否则返回 `false`。\n */\nexport function isNotFoundError(error: Error): boolean {\n return error.name === NOT_FOUND_ERROR;\n}\n\n/**\n * 将错误对象转换为 IOResult 类型。\n * @typeParam T - Result 的 Ok 类型。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n * @returns 转换后的 IOResult 对象。\n */\nexport function fileErrorToResult<T>(error: FileError): IOResult<T> {\n const err = miniGameFailureToError(error);\n\n if (isNotFoundFileError(err)) {\n err.name = NOT_FOUND_ERROR;\n }\n\n return Err(err);\n}\n\n/**\n * 处理 `mkdir` 的错误。\n */\nexport function fileErrorToMkdirResult(error: FileError): VoidIOResult {\n // 已存在当做成功\n return isAlreadyExistsFileError(error) ? RESULT_VOID : fileErrorToResult(error);\n}\n\n/**\n * 处理 `remove` 的错误。\n */\nexport function fileErrorToRemoveResult(error: FileError): VoidIOResult {\n // 目标 path 本就不存在,当做成功\n return isNotFoundFileError(error) ? RESULT_VOID : fileErrorToResult(error);\n}\n\n/*\n * 创建 `NothingToZipError` 错误。\n */\nexport function createNothingToZipError(): IOResult<never> {\n const error = new Error('Nothing to zip');\n error.name = NOTHING_TO_ZIP_ERROR;\n\n return Err(error);\n}\n\n/*\n * 创建\"文件不存在且 create 为 false\"的错误。\n * @param filePath - 文件路径。\n * @returns 错误结果。\n */\nexport function createFileNotExistsError(filePath: string): IOResult<never> {\n return Err(new Error(`Cannot append to non-existent file: ${filePath}`));\n}\n\n/*\n * 创建\"目录已存在但是文件\"的错误。\n * @param dirPath - 目录路径。\n * @returns 错误结果。\n */\nexport function createDirIsFileError(dirPath: string): IOResult<never> {\n return Err(new Error(`Path already exists but is a file: ${dirPath}`));\n}\n\n/**\n * 获取读取文件的编码。\n * @returns 返回 `'utf8'` 或 `undefined`(读取二进制时不传 encoding)。\n */\nexport function getReadFileEncoding(options?: ReadOptions): 'utf8' | undefined {\n // NOTE: 想要读取 ArrayBuffer 就不能传 encoding\n // 如果传了 'bytes' 或不传,返回 undefined\n return options?.encoding === 'utf8' ? 'utf8' : undefined;\n}\n\n/**\n * 获取写入文件的参数。\n */\nexport function getWriteFileContents(contents: WriteFileContent): IOResult<GetWriteFileContents> {\n const isBin = typeof contents !== 'string';\n\n let data: string | ArrayBuffer;\n\n if (isBin) {\n const result = tryResult(() => bufferSourceToAb(contents));\n if (result.isErr()) return result.asErr();\n\n data = result.unwrap();\n } else {\n data = contents;\n }\n\n const encoding = isBin ? undefined : 'utf8';\n\n return Ok({\n data,\n encoding,\n });\n}\n\n/**\n * 获取 `exists` 的结果。\n */\nexport function getExistsResult(statResult: IOResult<WechatMinigame.Stats>, options?: ExistsOptions): IOResult<boolean> {\n return statResult.map(stats => {\n const { isDirectory = false, isFile = false } = options ?? {};\n\n const notExist =\n (isDirectory && stats.isFile())\n || (isFile && stats.isDirectory());\n\n return !notExist;\n }).orElse(err => {\n return isNotFoundError(err) ? RESULT_FALSE : statResult.asErr();\n });\n}\n\n/**\n * 根据 `recursive` 不同标准化 `stat` 的结果(recursive=true 的时候开发者工具对于文件和空文件夹会返回单个 Stats)。\n * - `recursive=false`: 返回单个 `Stats` 或 `FileStats[]`\n * - `recursive=true`: 始终返回 `FileStats[]`,即使是单个文件或空目录\n * - 如果是单个 `Stats`,包装成数组,path 设为 '' 表示当前项目\n */\nexport function normalizeStats(statsOrFileStats: WechatMinigame.Stats | WechatMinigame.FileStats[], recursive: boolean): WechatMinigame.Stats | WechatMinigame.FileStats[] {\n if (Array.isArray(statsOrFileStats)) {\n return statsOrFileStats.map(({ path, stats }) => ({\n path: path.replace(/^\\/+/, ''), // 返回相对路径, 去掉开头的 `/`(安卓子项目 path 不以 `/` 开头)\n stats,\n })).sort((a, b) => a.path.localeCompare(b.path)); // 按 path 排序\n }\n\n // 只要是 recursive 就返回数组(即使是文件或者空目录))\n return recursive\n ? [{\n path: '', // 当前文件夹本身的相对路径\n stats: statsOrFileStats,\n }]\n : statsOrFileStats;\n}\n\n// #region Internal Types\n\ntype FileError = WechatMinigame.FileError | (Error & {\n errno?: number;\n});\n\ninterface GetWriteFileContents {\n data: string | ArrayBuffer;\n encoding?: 'utf8';\n}\n\n// #endregion\n\n// #region Internal Functions\n\n/**\n * 标准化同步或异步的文件错误对象。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction normalizeFileError(error: FileError): WechatMinigame.FileError {\n return error instanceof Error\n ? {\n errCode: error.errno ?? 0,\n errMsg: error.message,\n }\n : error;\n}\n\n/**\n * 判断是否文件或者文件夹不存在。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction isNotFoundFileError(error: FileError): boolean {\n // 1300002\tno such file or directory ${path}\n const { errCode, errMsg } = normalizeFileError(error);\n // 可能没有errCode\n return errCode === 1300002\n || errMsg.includes('no such file or directory');\n}\n\n/**\n * 判断是否文件或者文件夹已存在。\n * @param error - IO 操作的错误对象, 可以是同步(Error)或者异步(WechatMinigame.FileError)的。\n */\nfunction isAlreadyExistsFileError(error: FileError): boolean {\n // 1301005\tfile already exists ${dirPath}\t已有同名文件或目录\n const { errCode, errMsg } = normalizeFileError(error);\n // 可能没有errCode\n return errCode === 1301005\n || errMsg.includes('already exists');\n}\n\n/**\n * 验证 path 是否为字符串类型。\n * @param path - 待验证的路径。\n * @returns 如果不是字符串返回错误,否则返回 undefined。\n */\nfunction validatePathType(path: unknown): IOResult<never> | undefined {\n if (typeof path !== 'string') {\n return Err(new TypeError(`Path must be a string but received ${typeof path}`));\n }\n}\n\n// #endregion\n","/**\n * @internal\n * 小游戏平台的异步文件系统操作实现。\n */\n\nimport { ABORT_ERROR, FetchError, type FetchResult, type FetchTask } from '@happy-ts/fetch-t';\nimport { basename, dirname, SEPARATOR } from '../path/mod.ts';\nimport { zipSync as compressSync, type AsyncZippable } from 'fflate/browser';\nimport { type AppendOptions, type ExistsOptions, type WriteOptions, type ZipOptions } from 'happy-opfs';\nimport { Err, Ok, RESULT_VOID, tryResult, type AsyncIOResult, type AsyncVoidIOResult, type IOResult, type VoidIOResult } from 'happy-rusty';\nimport { Future } from 'tiny-future';\nimport { createFailedFetchTask, miniGameFailureToError, validateSafeUrl } from '../internal/mod.ts';\nimport { asyncResultify } from '../utils/mod.ts';\nimport type { ReadFileContent, ReadOptions, StatOptions, WriteFileContent } from './fs_define.ts';\nimport type { DownloadFileOptions, UploadFileOptions } from './mina_fs_define.ts';\nimport { createDirIsFileError, createFileNotExistsError, createNothingToZipError, EMPTY_BYTES, fileErrorToMkdirResult, fileErrorToRemoveResult, fileErrorToResult, getExistsResult, getFs, getReadFileEncoding, getUsrPath, getWriteFileContents, isNotFoundError, normalizeStats, validateAbsolutePath, validateExistsOptions, validateReadablePath, type ZipIOResult } from './mina_fs_shared.ts';\n\n/**\n * 递归创建文件夹,相当于`mkdir -p`。\n * @param dirPath - 需要创建的目录路径。\n * @returns 创建操作的异步结果。\n */\nexport async function mkdir(dirPath: string): AsyncVoidIOResult {\n // 为了检查根路径, 提前 validateAbsolutePath\n const dirPathRes = validateAbsolutePath(dirPath);\n if (dirPathRes.isErr()) return dirPathRes.asErr();\n dirPath = dirPathRes.unwrap();\n\n // 根目录无需创建\n if (dirPath === getUsrPath()) {\n return RESULT_VOID;\n }\n\n const statRes = await stat(dirPath);\n if (statRes.isOk()) {\n // 已存在并且是文件\n if (statRes.unwrap().isFile()) {\n return createDirIsFileError(dirPath);\n }\n\n // 存在文件夹则不创建\n return RESULT_VOID;\n }\n\n // 递归创建\n const mkdirRes = await asyncResultify(getFs().mkdir)({\n dirPath,\n recursive: true,\n });\n\n return mkdirRes\n .and(RESULT_VOID)\n .orElse(fileErrorToMkdirResult);\n}\n\n/**\n * 移动或重命名文件或目录。\n * @param srcPath - 原路径。\n * @param destPath - 新路径。\n * @returns 移动操作的异步结果。\n */\nexport async function move(srcPath: string, destPath: string): AsyncVoidIOResult {\n const srcPathRes = validateAbsolutePath(srcPath);\n if (srcPathRes.isErr()) return srcPathRes.asErr();\n srcPath = srcPathRes.unwrap();\n\n const destPathRes = validateAbsolutePath(destPath);\n if (destPathRes.isErr()) return destPathRes.asErr();\n destPath = destPathRes.unwrap();\n\n const moveRes = await asyncResultify(getFs().rename)({\n oldPath: srcPath,\n newPath: destPath,\n });\n\n return moveRes\n .and(RESULT_VOID)\n .orElse(fileErrorToResult);\n}\n\n/**\n * 读取目录下的所有文件和子目录。\n * @param dirPath - 目录路径。\n * @returns 包含目录内容的字符串数组的异步结果。\n */\nexport async function readDir(dirPath: string): AsyncIOResult<string[]> {\n const dirPathRes = validateReadablePath(dirPath);\n if (dirPathRes.isErr()) return dirPathRes.asErr();\n dirPath = dirPathRes.unwrap();\n\n const readDirRes = await asyncResultify(getFs().readdir)({\n dirPath,\n });\n\n return readDirRes\n .map(x => x.files)\n .orElse(fileErrorToResult);\n}\n\n/**\n * 以 UTF-8 格式读取文件。\n * @param filePath - 文件路径。\n * @param options - 读取选项,指定编码为 'utf8'。\n * @returns 包含文件内容的字符串的异步结果。\n */\nexport function readFile(filePath: string, options: ReadOptions & {\n encoding: 'utf8';\n}): AsyncIOResult<string>;\n\n/**\n * 以二进制格式读取文件。\n * @param filePath - 文件路径。\n * @param options - 读取选项,指定编码为 'bytes'。\n * @returns 包含文件内容的 Uint8Array<ArrayBuffer> 的异步结果。\n */\nexport function readFile(filePath: string, options?: ReadOptions & {\n encoding: 'bytes';\n}): AsyncIOResult<Uint8Array<ArrayBuffer>>;\n\n/**\n * 读取文件内容。\n * @param filePath - 文件路径。\n * @param options - 读取选项。\n * @returns 包含文件内容的异步结果。\n */\nexport function readFile(filePath: string, options?: ReadOptions): AsyncIOResult<ReadFileContent>;\n\n/**\n * 读取文件内容,可选地指定编码和返回类型。\n * @template T - 返回内容的类型。\n * @param filePath - 文件路径。\n * @param options - 可选的读取选项。\n * @returns 包含文件内容的异步结果。\n */\nexport async function readFile(filePath: string, options?: ReadOptions): AsyncIOResult<ReadFileContent> {\n const filePathRes = validateReadablePath(filePath);\n if (filePathRes.isErr()) return filePathRes.asErr();\n filePath = filePathRes.unwrap();\n\n const encoding = getReadFileEncoding(options);\n const readFileRes = await asyncResultify(getFs().readFile)({\n filePath,\n encoding,\n });\n\n return readFileRes\n .map(x => {\n const { data } = x;\n // 小游戏返回的是 ArrayBuffer,需要转换为 Uint8Array\n return typeof data === 'string' ? data : new Uint8Array(data);\n })\n .orElse(fileErrorToResult);\n}\n\n/**\n * 删除指定路径的文件或目录。\n * @param path - 需要删除的文件或目录的路径。\n * @returns 删除操作的异步结果。\n */\nexport async function remove(path: string): AsyncVoidIOResult {\n const statRes = await stat(path);\n if (statRes.isErr()) {\n // 不存在当做成功\n return isNotFoundError(statRes.unwrapErr()) ? RESULT_VOID : statRes.asErr();\n }\n\n // stat 已经校验通过了\n path = validateAbsolutePath(path).unwrap();\n\n // 文件夹还是文件\n const removeRes = await (statRes.unwrap().isDirectory()\n ? asyncResultify(getFs().rmdir)({\n dirPath: path,\n recursive: true,\n })\n : asyncResultify(getFs().unlink)({\n filePath: path,\n }));\n\n return removeRes\n .and(RESULT_VOID)\n .orElse(fileErrorToRemoveResult);\n}\n\n/**\n * 获取文件或目录的状态信息。\n * @param path - 文件或目录的路径。\n * @param options - 可选选项。\n * @returns 包含状态信息的异步结果。\n */\nexport function stat(path: string, options?: StatOptions & {\n recursive: false;\n}): AsyncIOResult<WechatMinigame.Stats>;\nexport function stat(path: string, options: StatOptions & {\n recursive: true;\n}): AsyncIOResult<WechatMinigame.FileStats[]>;\nexport function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]>;\nexport async function stat(path: string, options?: StatOptions): AsyncIOResult<WechatMinigame.Stats | WechatMinigame.FileStats[]> {\n const pathRes = validateReadablePath(path);\n if (pathRes.isErr()) return pathRes.asErr();\n path = pathRes.unwrap();\n\n const { recursive = false } = options ?? {};\n const statRes = await asyncResultify(getFs().stat)({\n path,\n recursive,\n });\n\n return statRes\n .map(x => normalizeStats(x.stats, recursive))\n .orElse(fileErrorToResult);\n}\n\n/**\n * 将内容写入文件。\n * @param filePath - 文件路径。\n * @param contents - 要写入的内容。\n * @param options - 可选的写入选项。\n * @returns 写入操作的异步结果。\n */\nexport async function writeFile(filePath: string, contents: WriteFileContent, options?: WriteOptions): AsyncVoidIOResult {\n // 默认创建\n const { append = false, create = true } = options ?? {};\n\n const fs = getFs();\n let writeMethod: typeof fs.appendFile | typeof fs.writeFile = fs.writeFile;\n\n if (append) {\n // append 先判断文件是否存在\n const existsRes = await exists(filePath);\n if (existsRes.isErr()) return existsRes.asErr();\n\n if (existsRes.unwrap()) {\n // 文件存在才使用 appendFile\n writeMethod = fs.appendFile;\n } else {\n // 文件不存在,根据 create 参数决定\n if (!create) {\n return createFileNotExistsError(filePath);\n }\n // create=true 时使用 writeFile 创建文件\n writeMethod = fs.writeFile;\n }\n }\n\n // 减少可能的重复校验\n const filePathRes = validateAbsolutePath(filePath);\n if (filePathRes.isErr()) return filePathRes.asErr();\n filePath = filePathRes.unwrap();\n\n // 使用 writeFile 时(文件不存在或非 append 模式)需要创建目录\n if (create && writeMethod === fs.writeFile) {\n const mkdirRes = await mkdir(dirname(filePath));\n if (mkdirRes.isErr()) return mkdirRes;\n }\n\n const contentsRes = getWriteFileContents(contents);\n if (contentsRes.isErr()) return contentsRes.asErr();\n const { data, encoding } = contentsRes.unwrap();\n\n const writeRes = await asyncResultify(writeMethod)({\n filePath,\n data,\n encoding,\n });\n\n return writeRes\n .and(RESULT_VOID)\n .orElse(fileErrorToResult);\n}\n\n/**\n * 向文件追加内容。\n * @param filePath - 文件路径。\n * @param contents - 要追加的内容。\n * @param options - 可选的追加选项。\n * @returns 追加操作的异步结果。\n */\nexport function appendFile(filePath: string, contents: WriteFileContent, options?: AppendOptions): AsyncVoidIOResult {\n return writeFile(filePath, contents, {\n append: true,\n create: options?.create ?? true,\n });\n}\n\n/**\n * 复制文件或文件夹。\n *\n * @param srcPath - 源文件或文件夹路径。\n * @param destPath - 目标文件或文件夹路径。\n * @returns 操作的异步结果。\n */\nexport async function copy(srcPath: string, destPath: string): AsyncVoidIOResult {\n const destPathRes = validateAbsolutePath(destPath);\n if (destPathRes.isErr()) return destPathRes.asErr();\n destPath = destPathRes.unwrap();\n\n const statRes = await stat(srcPath, {\n recursive: true,\n });\n if (statRes.isErr()) return statRes.asErr();\n\n // stat 已经校验通过了\n srcPath = validateAbsolutePath(srcPath).unwrap();\n\n for (const { path, stats } of statRes.unwrap()) {\n let copyRes: VoidIOResult;\n\n if (!path) {\n // 根目录或者文件\n if (stats.isDirectory()) {\n copyRes = await mkdir(destPath);\n } else {\n const mkdirRes = await mkdir(dirname(destPath));\n copyRes = await mkdirRes.andThenAsync(() => {\n return copyFile(srcPath, destPath);\n });\n }\n } else {\n // 不能用join\n const srcEntryPath = srcPath + SEPARATOR + path;\n const destEntryPath = destPath + SEPARATOR + path;\n\n copyRes = await (stats.isDirectory()\n ? mkdir(destEntryPath)\n // 由于串行执行, 文件的父目录一定先于文件创建, 所以不需要额外 mkdir\n : copyFile(srcEntryPath, destEntryPath));\n }\n\n if (copyRes.isErr()) return copyRes;\n }\n\n return RESULT_VOID;\n}\n\n/**\n * 检查指定路径的文件或目录是否存在。\n * @param path - 文件或目录的路径。\n * @param options - 可选的检查选项。\n * @returns 检查存在性的异步结果,存在时返回 true。\n */\nexport async function exists(path: string, options?: ExistsOptions): AsyncIOResult<boolean> {\n const optionsRes = validateExistsOptions(options);\n if (optionsRes.isErr()) return optionsRes.asErr();\n\n const statRes = await stat(path);\n return getExistsResult(statRes, options);\n}\n\n/**\n * 清空目录中的所有文件和子目录。\n * @param dirPath - 目录路径。\n * @returns 清空操作的异步结果。\n */\nexport async function emptyDir(dirPath: string): AsyncVoidIOResult {\n const readDirRes = await readDir(dirPath);\n if (readDirRes.isErr()) {\n // 不存在则创建\n return isNotFoundError(readDirRes.unwrapErr())\n ? mkdir(dirPath)\n : readDirRes.asErr();\n }\n\n // readDir 已经校验通过了\n dirPath = validateAbsolutePath(dirPath).unwrap();\n\n const tasks = readDirRes.unwrap().map(name => remove(dirPath + SEPARATOR + name));\n const taskResults = await Promise.all(tasks);\n // 是否有失败?\n const failed = taskResults.find(x => x.isErr());\n\n return failed ?? RESULT_VOID;\n}\n\n/**\n * 读取文本文件的内容。\n * @param filePath - 文件路径。\n * @returns 包含文件文本内容的异步结果。\n */\nexport