@planet-matrix/mobius-model 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +134 -21
  3. package/dist/index.js +45 -4
  4. package/dist/index.js.map +186 -11
  5. package/oxlint.config.ts +6 -0
  6. package/package.json +16 -10
  7. package/src/abort/README.md +92 -0
  8. package/src/abort/abort-manager.ts +278 -0
  9. package/src/abort/abort-signal-listener-manager.ts +81 -0
  10. package/src/abort/index.ts +2 -0
  11. package/src/basic/README.md +69 -117
  12. package/src/basic/enhance.ts +10 -0
  13. package/src/basic/function.ts +81 -62
  14. package/src/basic/index.ts +2 -0
  15. package/src/basic/is.ts +152 -71
  16. package/src/basic/object.ts +82 -0
  17. package/src/basic/promise.ts +29 -8
  18. package/src/basic/string.ts +2 -33
  19. package/src/color/README.md +105 -0
  20. package/src/color/index.ts +3 -0
  21. package/src/color/internal.ts +42 -0
  22. package/src/color/rgb/analyze.ts +236 -0
  23. package/src/color/rgb/construct.ts +130 -0
  24. package/src/color/rgb/convert.ts +227 -0
  25. package/src/color/rgb/derive.ts +303 -0
  26. package/src/color/rgb/index.ts +6 -0
  27. package/src/color/rgb/internal.ts +208 -0
  28. package/src/color/rgb/parse.ts +302 -0
  29. package/src/color/rgb/serialize.ts +144 -0
  30. package/src/color/types.ts +57 -0
  31. package/src/color/xyz/analyze.ts +80 -0
  32. package/src/color/xyz/construct.ts +19 -0
  33. package/src/color/xyz/convert.ts +71 -0
  34. package/src/color/xyz/index.ts +3 -0
  35. package/src/color/xyz/internal.ts +23 -0
  36. package/src/css/README.md +93 -0
  37. package/src/css/class.ts +559 -0
  38. package/src/css/index.ts +1 -0
  39. package/src/encoding/README.md +92 -0
  40. package/src/encoding/base64.ts +107 -0
  41. package/src/encoding/index.ts +1 -0
  42. package/src/environment/README.md +97 -0
  43. package/src/environment/basic.ts +26 -0
  44. package/src/environment/device.ts +311 -0
  45. package/src/environment/feature.ts +285 -0
  46. package/src/environment/geo.ts +337 -0
  47. package/src/environment/index.ts +7 -0
  48. package/src/environment/runtime.ts +400 -0
  49. package/src/environment/snapshot.ts +60 -0
  50. package/src/environment/variable.ts +239 -0
  51. package/src/event/README.md +90 -0
  52. package/src/event/class-event-proxy.ts +228 -0
  53. package/src/event/common.ts +19 -0
  54. package/src/event/event-manager.ts +203 -0
  55. package/src/event/index.ts +4 -0
  56. package/src/event/instance-event-proxy.ts +186 -0
  57. package/src/event/internal.ts +24 -0
  58. package/src/exception/README.md +96 -0
  59. package/src/exception/browser.ts +219 -0
  60. package/src/exception/index.ts +4 -0
  61. package/src/exception/nodejs.ts +169 -0
  62. package/src/exception/normalize.ts +106 -0
  63. package/src/exception/types.ts +99 -0
  64. package/src/identifier/README.md +92 -0
  65. package/src/identifier/id.ts +119 -0
  66. package/src/identifier/index.ts +2 -0
  67. package/src/identifier/uuid.ts +187 -0
  68. package/src/index.ts +18 -1
  69. package/src/log/README.md +79 -0
  70. package/src/log/index.ts +5 -0
  71. package/src/log/log-emitter.ts +72 -0
  72. package/src/log/log-record.ts +10 -0
  73. package/src/log/log-scheduler.ts +74 -0
  74. package/src/log/log-type.ts +8 -0
  75. package/src/log/logger.ts +543 -0
  76. package/src/orchestration/README.md +89 -0
  77. package/src/orchestration/coordination/barrier.ts +214 -0
  78. package/src/orchestration/coordination/count-down-latch.ts +215 -0
  79. package/src/orchestration/coordination/errors.ts +98 -0
  80. package/src/orchestration/coordination/index.ts +16 -0
  81. package/src/orchestration/coordination/internal/wait-constraints.ts +95 -0
  82. package/src/orchestration/coordination/internal/wait-queue.ts +109 -0
  83. package/src/orchestration/coordination/keyed-lock.ts +168 -0
  84. package/src/orchestration/coordination/mutex.ts +257 -0
  85. package/src/orchestration/coordination/permit.ts +127 -0
  86. package/src/orchestration/coordination/read-write-lock.ts +444 -0
  87. package/src/orchestration/coordination/semaphore.ts +280 -0
  88. package/src/orchestration/index.ts +1 -0
  89. package/src/random/README.md +78 -0
  90. package/src/random/index.ts +1 -0
  91. package/src/random/string.ts +35 -0
  92. package/src/reactor/README.md +4 -0
  93. package/src/reactor/reactor-core/primitive.ts +9 -9
  94. package/src/reactor/reactor-core/reactive-system.ts +5 -5
  95. package/src/singleton/README.md +79 -0
  96. package/src/singleton/factory.ts +55 -0
  97. package/src/singleton/index.ts +2 -0
  98. package/src/singleton/manager.ts +204 -0
  99. package/src/storage/README.md +107 -0
  100. package/src/storage/index.ts +1 -0
  101. package/src/storage/table.ts +449 -0
  102. package/src/timer/README.md +86 -0
  103. package/src/timer/expiration/expiration-manager.ts +594 -0
  104. package/src/timer/expiration/index.ts +3 -0
  105. package/src/timer/expiration/min-heap.ts +208 -0
  106. package/src/timer/expiration/remaining-manager.ts +241 -0
  107. package/src/timer/index.ts +1 -0
  108. package/src/type/README.md +54 -307
  109. package/src/type/class.ts +2 -2
  110. package/src/type/index.ts +14 -14
  111. package/src/type/is.ts +265 -2
  112. package/src/type/object.ts +37 -0
  113. package/src/type/string.ts +7 -2
  114. package/src/type/tuple.ts +6 -6
  115. package/src/type/union.ts +16 -0
  116. package/src/web/README.md +77 -0
  117. package/src/web/capture.ts +35 -0
  118. package/src/web/clipboard.ts +97 -0
  119. package/src/web/dom.ts +117 -0
  120. package/src/web/download.ts +16 -0
  121. package/src/web/event.ts +46 -0
  122. package/src/web/index.ts +10 -0
  123. package/src/web/local-storage.ts +113 -0
  124. package/src/web/location.ts +28 -0
  125. package/src/web/permission.ts +172 -0
  126. package/src/web/script-loader.ts +432 -0
  127. package/tests/unit/abort/abort-manager.spec.ts +225 -0
  128. package/tests/unit/abort/abort-signal-listener-manager.spec.ts +62 -0
  129. package/tests/unit/basic/array.spec.ts +1 -1
  130. package/tests/unit/basic/object.spec.ts +32 -1
  131. package/tests/unit/basic/stream.spec.ts +1 -1
  132. package/tests/unit/basic/string.spec.ts +0 -9
  133. package/tests/unit/color/rgb/analyze.spec.ts +110 -0
  134. package/tests/unit/color/rgb/construct.spec.ts +56 -0
  135. package/tests/unit/color/rgb/convert.spec.ts +60 -0
  136. package/tests/unit/color/rgb/derive.spec.ts +103 -0
  137. package/tests/unit/color/rgb/parse.spec.ts +66 -0
  138. package/tests/unit/color/rgb/serialize.spec.ts +46 -0
  139. package/tests/unit/color/xyz/analyze.spec.ts +33 -0
  140. package/tests/unit/color/xyz/construct.spec.ts +10 -0
  141. package/tests/unit/color/xyz/convert.spec.ts +18 -0
  142. package/tests/unit/css/class.spec.ts +157 -0
  143. package/tests/unit/encoding/base64.spec.ts +40 -0
  144. package/tests/unit/environment/basic.spec.ts +20 -0
  145. package/tests/unit/environment/device.spec.ts +146 -0
  146. package/tests/unit/environment/feature.spec.ts +388 -0
  147. package/tests/unit/environment/geo.spec.ts +111 -0
  148. package/tests/unit/environment/runtime.spec.ts +364 -0
  149. package/tests/unit/environment/snapshot.spec.ts +4 -0
  150. package/tests/unit/environment/variable.spec.ts +190 -0
  151. package/tests/unit/event/class-event-proxy.spec.ts +225 -0
  152. package/tests/unit/event/event-manager.spec.ts +246 -0
  153. package/tests/unit/event/instance-event-proxy.spec.ts +187 -0
  154. package/tests/unit/exception/browser.spec.ts +213 -0
  155. package/tests/unit/exception/nodejs.spec.ts +144 -0
  156. package/tests/unit/exception/normalize.spec.ts +57 -0
  157. package/tests/unit/identifier/id.spec.ts +71 -0
  158. package/tests/unit/identifier/uuid.spec.ts +85 -0
  159. package/tests/unit/log/log-emitter.spec.ts +33 -0
  160. package/tests/unit/log/log-scheduler.spec.ts +40 -0
  161. package/tests/unit/log/log-type.spec.ts +7 -0
  162. package/tests/unit/log/logger.spec.ts +222 -0
  163. package/tests/unit/orchestration/coordination/barrier.spec.ts +96 -0
  164. package/tests/unit/orchestration/coordination/count-down-latch.spec.ts +63 -0
  165. package/tests/unit/orchestration/coordination/errors.spec.ts +29 -0
  166. package/tests/unit/orchestration/coordination/keyed-lock.spec.ts +109 -0
  167. package/tests/unit/orchestration/coordination/mutex.spec.ts +132 -0
  168. package/tests/unit/orchestration/coordination/permit.spec.ts +43 -0
  169. package/tests/unit/orchestration/coordination/read-write-lock.spec.ts +154 -0
  170. package/tests/unit/orchestration/coordination/semaphore.spec.ts +135 -0
  171. package/tests/unit/random/string.spec.ts +11 -0
  172. package/tests/unit/reactor/alien-signals-effect.spec.ts +11 -10
  173. package/tests/unit/reactor/preact-signal.spec.ts +1 -2
  174. package/tests/unit/singleton/singleton.spec.ts +49 -0
  175. package/tests/unit/storage/table.spec.ts +620 -0
  176. package/tests/unit/timer/expiration/expiration-manager.spec.ts +464 -0
  177. package/tests/unit/timer/expiration/min-heap.spec.ts +71 -0
  178. package/tests/unit/timer/expiration/remaining-manager.spec.ts +234 -0
  179. package/.oxlintrc.json +0 -5
@@ -0,0 +1,107 @@
1
+ const internalBase64Pattern = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
2
+ const internalNormalizeBase64 = (input: string): string => {
3
+ return input.replaceAll(/\s+/g, "")
4
+ }
5
+ /**
6
+ * 判断输入是否为合法的 Base64 字符串。
7
+ *
8
+ * @example
9
+ * ```
10
+ * // Expect: true
11
+ * const example1 = isBase64("aGVsbG8=")
12
+ * // Expect: false
13
+ * const example2 = isBase64("abc")
14
+ * ```
15
+ */
16
+ export const isBase64 = (input: string): boolean => {
17
+ const normalizedInput = internalNormalizeBase64(input)
18
+ return internalBase64Pattern.test(normalizedInput)
19
+ }
20
+
21
+ const internalAssertValidBase64 = (input: string): void => {
22
+ if (internalBase64Pattern.test(input) === false) {
23
+ throw new TypeError("Invalid Base64 input")
24
+ }
25
+ }
26
+ /**
27
+ * 断言输入是合法的 Base64 字符串。
28
+ *
29
+ * @example
30
+ * ```
31
+ * const example1 = assertBase64("aGVsbG8=")
32
+ * // Expect: undefined
33
+ *
34
+ * // Expect: throws TypeError
35
+ * const example2 = () => assertBase64("abc")
36
+ * ```
37
+ *
38
+ * @throws {TypeError} when input is not valid Base64
39
+ */
40
+ export const assertBase64 = (input: string): void => {
41
+ const normalizedInput = internalNormalizeBase64(input)
42
+ internalAssertValidBase64(normalizedInput)
43
+ }
44
+
45
+ const internalStringToBase64ByBrowserApi = (input: string): string => {
46
+ const bytes = new TextEncoder().encode(input)
47
+ let binaryString = ""
48
+
49
+ bytes.forEach((byte) => {
50
+ binaryString = binaryString + String.fromCodePoint(byte)
51
+ })
52
+
53
+ return btoa(binaryString)
54
+ }
55
+ /**
56
+ * 将 UTF-8 字符串转换为 Base64 字符串。
57
+ *
58
+ * @example
59
+ * ```
60
+ * // Expect: "aGVsbG8="
61
+ * const example1 = stringToBase64("hello")
62
+ * // Expect: "5L2g5aW9"
63
+ * const example2 = stringToBase64("你好")
64
+ * ```
65
+ */
66
+ export const stringToBase64 = (input: string): string => {
67
+ if (typeof Buffer !== "undefined") {
68
+ return Buffer.from(input, "utf8").toString("base64")
69
+ }
70
+
71
+ if (typeof btoa !== "undefined") {
72
+ return internalStringToBase64ByBrowserApi(input)
73
+ }
74
+
75
+ throw new Error("No Base64 runtime support found")
76
+ }
77
+
78
+ const internalBase64ToStringByBrowserApi = (input: string): string => {
79
+ const binaryString = atob(input)
80
+ const bytes = Uint8Array.from(binaryString, char => char.codePointAt(0) ?? 0)
81
+ return new TextDecoder().decode(bytes)
82
+ }
83
+ /**
84
+ * 将合法的 Base64 字符串转换为 UTF-8 字符串。
85
+ *
86
+ * @example
87
+ * ```
88
+ * // Expect: "hello"
89
+ * const example1 = base64ToString("aGVsbG8=")
90
+ * // Expect: "你好"
91
+ * const example2 = base64ToString("5L2g5aW9")
92
+ * ```
93
+ */
94
+ export const base64ToString = (input: string): string => {
95
+ const normalizedInput = internalNormalizeBase64(input)
96
+ internalAssertValidBase64(normalizedInput)
97
+
98
+ if (typeof Buffer !== "undefined") {
99
+ return Buffer.from(normalizedInput, "base64").toString("utf8")
100
+ }
101
+
102
+ if (typeof atob !== "undefined") {
103
+ return internalBase64ToStringByBrowserApi(normalizedInput)
104
+ }
105
+
106
+ throw new Error("No Base64 runtime support found")
107
+ }
@@ -0,0 +1 @@
1
+ export * from "./base64.ts"
@@ -0,0 +1,97 @@
1
+ # Environment
2
+
3
+ ## Description
4
+
5
+ Environment 模块提供围绕运行环境(environment)进行识别、描述、访问和校验的基础能力,用于统一处理运行时(runtime)、宿主能力(feature)、设备信息(device)、地理信息(geo)、环境变量(environment variable)以及环境快照(snapshot)等问题域。
6
+
7
+ 它关注的是“当前处于什么环境、可以读取哪些环境上下文、这些环境信息如何被安全表达”,而不是基于这些信息替调用方做业务决策。
8
+
9
+ ## For Understanding
10
+
11
+ 理解 Environment 模块时,应先明确它的职责边界:它负责识别环境、描述环境、读取环境,而不负责操纵环境或消费环境事件。也就是说,这个模块可以帮助调用方判断当前是不是浏览器、Node.js、Bun、Deno、Web Worker 或 Service Worker,可以帮助访问 `navigator`、`document`、`process` 这类宿主对象,也可以帮助读取设备、地理、变量与快照信息;但它不应直接承担全局异常监听、事件订阅注册、日志上报、状态恢复或业务分支策略。
12
+
13
+ 当前模块可以理解为几类相互关联但边界清楚的子问题域:
14
+
15
+ - 运行时识别与上下文获取:用于检测当前运行时、暴露相应上下文,并为分支执行提供 `use...` 风格入口。
16
+ - 宿主能力访问:用于判断和获取 `process`、`navigator`、`document`、`CSS`、权限(permissions)等特定宿主能力。
17
+ - 设备与用户代理信息:用于从 `navigator`、`screen` 或传入的 user agent 中整理稳定的设备描述。
18
+ - 地理信息:用于从外部服务拉取 IP 或地理位置相关信息。
19
+ - 变量与配置:用于解析、加载、校验环境变量,并把变量读取与模式(schema)验证组合起来。
20
+ - 快照与诊断:用于把多种环境信息组合成更适合诊断或输出的快照视图。
21
+
22
+ 只要问题的核心仍是“当前环境是什么、有哪些可用上下文、环境信息如何被读出并表达”,它就适合属于 Environment。反之,如果问题已经变成“拿到环境后如何驱动宿主行为”,那通常就应该交给其它模块。
23
+
24
+ ## For Using
25
+
26
+ 当应用需要在跨运行时代码中做分支、访问宿主上下文、整理设备信息、读取环境变量或采集诊断快照时,可以使用这个模块。它尤其适合放在应用入口、基础设施层和适配层,而不是深入业务语义最内层。
27
+
28
+ 当前公共能力大致可以按以下几类理解:
29
+
30
+ - 运行时识别与条件执行:例如识别浏览器、Node.js、Bun、Deno、Worker 等运行时,并按运行时安全获取上下文或分发逻辑。
31
+ - 宿主能力访问:例如判断并获取 `process`、`navigator`、`clipboard`、`permissions`、`document`、`CSS` 等宿主对象。
32
+ - 设备与 user agent 分析:例如读取设备导航信息、屏幕信息,或把 user agent 解析为更稳定的设备描述。
33
+ - 地理信息查询:例如聚合外部服务提供的 IP 与地理位置信息,用于诊断、展示或环境补充。
34
+ - 环境变量读取与校验:例如解析变量文本、读取变量宿主、加载变量、结合 schema 做结构化验证。
35
+ - 环境快照:例如把当前运行时、设备、变量等信息整理成适合输出、调试或诊断的综合视图。
36
+
37
+ 调用方在使用时仍应处理宿主差异与降级路径。Environment 的职责是让这些差异更容易被表达和访问,而不是假装所有环境都完全一致。
38
+
39
+ ## For Contributing
40
+
41
+ 贡献 Environment 模块时,应优先判断新增能力是否真的表达了稳定、可复用、跨项目成立的环境语义。如果它只是某个业务流程中的一次性判断,或者它的核心已经变成宿主交互副作用,那么通常不应进入这个模块。
42
+
43
+ 在扩展时,应优先遵守以下边界:
44
+
45
+ - 这里负责识别环境、读取环境、描述环境,而不负责全局监听、日志发送、状态恢复或其它策略层行为。
46
+ - 运行时识别与宿主能力访问应保持清楚分层,不要把“能否访问某对象”和“拿到对象后做什么”混成一个入口。
47
+ - 设备、地理、变量、快照等子问题域可以共存,但都应回到统一的环境语义,而不是各自演变成独立杂项工具。
48
+ - 公共能力应优先让缺失环境时的失败语义清楚可见,而不是通过隐式降级掩盖上下文不可用的事实。
49
+
50
+ ### JSDoc 注释格式要求
51
+
52
+ - 每个公开导出的目标(类型、函数、变量、类等)都应包含 JSDoc 注释,让人在不跳转实现的情况下就能理解用途。
53
+ - JSDoc 注释第一行应为清晰且简洁的描述,该描述优先使用中文(英文也可以)。
54
+ - 如果描述后还有其他内容,应在描述后加一个空行。
55
+ - 如果有示例,应使用 `@example` 标签,后接三重反引号代码块(不带语言标识)。
56
+ - 如果有示例,应包含多个场景,展示不同用法,尤其要覆盖常见组合方式或边界输入。
57
+ - 如果有示例,应使用注释格式说明每个场景:`// Expect: <result>`。
58
+ - 如果有示例,应将结果赋值给 `example1`、`example2` 之类的变量,以保持示例易读。
59
+ - 如果有示例,`// Expect: <result>` 应该位于 `example1`、`example2` 之前,以保持示例的逻辑清晰。
60
+ - 如果有示例,应优先使用确定性示例;避免断言精确的随机输出。
61
+ - 如果函数返回结构化字符串,应展示其预期格式特征。
62
+ - 如果有参考资料,应将 `@see` 放在 `@example` 代码块之后,并用一个空行分隔。
63
+
64
+ ### 实现规范要求
65
+
66
+ - 不同程序元素之间使用一个空行分隔,保持结构清楚。这里的程序元素,通常指函数、类型、常量,以及直接服务于它们的辅助元素。
67
+ - 某程序元素独占的辅助元素与该程序元素本身视为一个整体,不要在它们之间添加空行。
68
+ - 程序元素的辅助元素应该放置在该程序元素的上方,以保持阅读时的逻辑顺序。
69
+ - 若辅助元素被多个程序元素共享,则应将其视为独立的程序元素,放在这些程序元素中第一个相关目标的上方,并与后续程序元素之间保留一个空行。
70
+ - 辅助元素也应该像其它程序元素一样,保持清晰的命名和适当的注释,以便在需要阅读实现细节时能够快速理解它们的作用和使用方式。
71
+ - 辅助元素的命名必须以前缀 `internal` 开头(或 `Internal`,大小写不敏感)。
72
+ - 辅助元素永远不要公开导出。
73
+ - 被模块内多个不同文件中的程序元素共享的辅助元素,应该放在一个单独的文件中,例如 `./src/environment/internal.ts`;若共享仅存在于某个子问题域内部,也可以放在对应文件或子模块中。
74
+ - 模块内可以包含子模块。只有当某个子目录表达一个稳定、可单独理解、且可能被父模块重导出的子问题域时,才应将其视为子模块。
75
+ - 子模块包含多个文件时,应该为其单独创建子文件夹,并为其创建单独的 Barrel 文件;父模块的 Barrel 文件再重导出子模块的 Barrel 文件。
76
+ - 子模块不需要有自己的 `README.md`。
77
+ - 子模块可以有自己的 `internal.ts` 文件,多个子模块共享的辅助元素应该放在父模块的 `internal.ts` 文件中,单个子模块共享的辅助元素应该放在该子模块的 `internal.ts` 文件中。
78
+ - 对模块依赖关系的要求(通常是不循环依赖或不反向依赖)与对 DRY 的要求可能产生冲突。此时,若复用的代码数量不大,可以适当牺牲 DRY,复制粘贴并保留必要的注释说明;若复用的代码数量较大,则可以将其抽象到新的文件或子模块中,如 `common.ts`,并在需要的地方导入使用。
79
+ - 实现应优先保持检测逻辑、上下文获取逻辑与后续使用逻辑解耦,避免让单一入口同时承担过多副作用和策略判断。
80
+
81
+ ### 导出策略要求
82
+
83
+ - 保持内部辅助项和内部符号为私有,不要让外部接入依赖临时性的内部结构。
84
+ - 每个模块都应有一个用于重导出所有公共 API 的 Barrel 文件。
85
+ - Barrel 文件应命名为 `index.ts`,放在模块目录根部,并且所有公共 API 都应从该文件导出。
86
+ - 新增公共能力时,应优先检查它是否表达稳定、清楚且值得长期维护的环境语义,而不是某段实现细节的便捷暴露;仅在确认需要长期对外承诺时再加入 Barrel 导出。
87
+
88
+ ### 测试要求
89
+
90
+ - 若程序元素是函数,则只为该函数编写一个测试,如果该函数需要测试多个用例,应放在同一个测试中。
91
+ - 若程序元素是类,则至少要为该类的每一个方法编写一个测试,如果该方法需要测试多个用例,应放在同一个测试中。
92
+ - 若程序元素是类,除了为该类的每一个方法编写至少一个测试之外,还可以为该类编写任意多个测试,以覆盖该类的不同使用场景或边界情况。
93
+ - 若编写测试时需要用到辅助元素(Mock 或 Spy 等),可以在测试文件中直接定义这些辅助元素。若辅助元素较为简单,则可以直接放在每一个测试内部,优先保证每个测试的独立性,而不是追求极致 DRY;若辅助元素较为复杂或需要在多个测试中复用,则可以放在测试文件顶部,供该测试文件中的所有测试使用。
94
+ - 测试顺序应与源文件中被测试目标的原始顺序保持一致。
95
+ - 若该模块不需要测试,必须在说明文件中明确说明该模块不需要测试,并说明理由。一般来说,只有在该模块没有可执行的公共函数、只承载类型层表达,或其语义已被上层模块的测试完整覆盖且重复测试几乎不再带来额外价值时,才适合这样处理。
96
+ - 模块的单元测试文件目录是 `./tests/unit/environment`,若模块包含子模块,则子模块的单元测试文件目录为 `./tests/unit/environment/<sub-module-name>`。
97
+ - 测试应重点覆盖运行时识别、宿主能力可用性、缺失上下文时的失败语义、变量校验路径,以及设备与快照输出是否保持稳定可解释。
@@ -0,0 +1,26 @@
1
+
2
+ /**
3
+ * Define a conditional usage function for a target object.
4
+ */
5
+ export type Use<Target> = <R>(
6
+ use: (target: Target) => R,
7
+ otherwise: () => R
8
+ ) => R
9
+ /**
10
+ * Create a conditional usage helper from getter and availability checks.
11
+ */
12
+ export const useFactory = <Target>(
13
+ get: () => Target,
14
+ has: () => boolean,
15
+ ): Use<Target> => {
16
+ const use = <R>(use: (target: Target) => R, otherwise: () => R): R => {
17
+ if (has() === true) {
18
+ const target = get();
19
+ return use(target);
20
+ } else {
21
+ return otherwise();
22
+ }
23
+ }
24
+
25
+ return use;
26
+ }
@@ -0,0 +1,311 @@
1
+ import type { BrowserName, BrowserType, CPUArch, DeviceType, DeviceVendor, EngineName, Extension, OSName } from "ua-parser-js/enums"
2
+
3
+ import { UAParser } from "ua-parser-js"
4
+ import { isAppleSilicon } from "ua-parser-js/device-detection"
5
+ import { isFrozenUA } from "ua-parser-js/helpers"
6
+ import { isAIAssistant, isAICrawler, isBot } from "ua-parser-js/bot-detection"
7
+ import { isChromeFamily, isElectron, isFromEU, isStandalonePWA } from "ua-parser-js/browser-detection"
8
+
9
+ import { Bots, CLIs, Crawlers, Emails, ExtraDevices, Fetchers, InApps, Libraries, MediaPlayers, Vehicles } from "ua-parser-js/extensions"
10
+
11
+ import type { IResult } from "ua-parser-js"
12
+
13
+ interface NetworkInformation {
14
+ downlink: number
15
+ downlinkMax: number
16
+ effectiveType: "slow-2g" | "2g" | "3g" | "4g"
17
+ rtt: number
18
+ saveData: boolean
19
+ type: "bluetooth" | "cellular" | "ethernet" | "none" | "wifi" | "wimax" | "other" | "unknown"
20
+ }
21
+ const getNetworkInformation = (): NetworkInformation | undefined => {
22
+ let connection: NetworkInformation | undefined;
23
+ if ("connection" in navigator) {
24
+ // oxlint-disable-next-line no-unsafe-type-assertion
25
+ connection = navigator.connection as NetworkInformation;
26
+ } else if ("mozConnection" in navigator) {
27
+ // oxlint-disable-next-line no-unsafe-type-assertion
28
+ connection = navigator.mozConnection as NetworkInformation;
29
+ } else if ("webkitConnection" in navigator) {
30
+ // oxlint-disable-next-line no-unsafe-type-assertion
31
+ connection = navigator.webkitConnection as NetworkInformation;
32
+ }
33
+ return connection;
34
+ };
35
+
36
+ /**
37
+ * Describe navigator-level device and network metadata.
38
+ */
39
+ export interface DeviceNavigatorInfo {
40
+ oscpu: string | undefined
41
+ deviceMemory: number | undefined
42
+ hardwareConcurrency: number | undefined
43
+ maxTouchPoints: number | undefined
44
+ platform: Navigator['platform']
45
+
46
+ devicePosture: "continuous" | "folded" | undefined
47
+ connectionDownlink: NetworkInformation['downlink'] | undefined
48
+ connectionDownlinkMax: NetworkInformation['downlinkMax'] | undefined
49
+ connectionEffectiveType: NetworkInformation['effectiveType'] | undefined
50
+ connectionRtt: NetworkInformation['rtt'] | undefined
51
+ connectionSaveData: NetworkInformation['saveData'] | undefined
52
+ connectionType: NetworkInformation['type'] | undefined
53
+ online: boolean
54
+ cookiesEnabled: boolean
55
+ language: string
56
+ languages: string[]
57
+ pdfViewerEnabled: boolean
58
+ }
59
+ /**
60
+ * Collect device information from the Navigator API.
61
+ */
62
+ export const getDeviceNavigatorInfo = (): DeviceNavigatorInfo => {
63
+ const connection = getNetworkInformation();
64
+
65
+ return {
66
+ oscpu: "oscpu" in navigator ? String(navigator.oscpu) : undefined,
67
+ hardwareConcurrency: "hardwareConcurrency" in navigator ? Number.parseInt(String(navigator.hardwareConcurrency), 10) : undefined,
68
+ deviceMemory: "deviceMemory" in navigator ? Number.parseFloat(String(navigator.deviceMemory)) : undefined,
69
+ maxTouchPoints: navigator.maxTouchPoints,
70
+ platform: navigator.platform,
71
+
72
+ // oxlint-disable-next-line no-unsafe-type-assertion
73
+ devicePosture: "devicePosture" in navigator ? (navigator.devicePosture as { type: "continuous" | "folded" }).type : undefined,
74
+ connectionDownlink: connection?.downlink ?? undefined,
75
+ connectionDownlinkMax: connection?.downlinkMax ?? undefined,
76
+ connectionEffectiveType: connection?.effectiveType ?? undefined,
77
+ connectionRtt: connection?.rtt ?? undefined,
78
+ connectionSaveData: connection?.saveData ?? undefined,
79
+ connectionType: connection?.type ?? undefined,
80
+ online: navigator.onLine,
81
+ cookiesEnabled: navigator.cookieEnabled,
82
+ language: navigator.language,
83
+ languages: [...navigator.languages],
84
+ pdfViewerEnabled: navigator.pdfViewerEnabled,
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Describe device metadata parsed from a user-agent string.
90
+ */
91
+ export interface DeviceUserAgentInfo {
92
+ ua: string
93
+ uaIsFrozen: boolean
94
+
95
+ cpuArchitecture: typeof CPUArch[keyof typeof CPUArch] | undefined
96
+
97
+ osName: typeof OSName[keyof typeof OSName] | undefined
98
+ osVersion: IResult['os']['version'] | undefined
99
+
100
+ engineName: typeof EngineName[keyof typeof EngineName] | undefined
101
+ engineVersion: IResult['engine']['version'] | undefined
102
+
103
+ browserName:
104
+ | typeof BrowserName[keyof typeof BrowserName]
105
+ | typeof Extension['BrowserName']['CLI'][keyof typeof Extension['BrowserName']['CLI']]
106
+ | typeof Extension['BrowserName']['Crawler'][keyof typeof Extension['BrowserName']['Crawler']]
107
+ | typeof Extension['BrowserName']['Email'][keyof typeof Extension['BrowserName']['Email']]
108
+ | typeof Extension['BrowserName']['Fetcher'][keyof typeof Extension['BrowserName']['Fetcher']]
109
+ | typeof Extension['BrowserName']['InApp'][keyof typeof Extension['BrowserName']['InApp']]
110
+ | typeof Extension['BrowserName']['Library'][keyof typeof Extension['BrowserName']['Library']]
111
+ | undefined
112
+ browserVersion: IResult['browser']['version'] | undefined
113
+ browserMajor: IResult['browser']['major'] | undefined
114
+ browserType: typeof BrowserType[keyof typeof BrowserType] | undefined
115
+ browserIsAiAssistant: boolean
116
+ browserIsAiCrawler: boolean
117
+ browserIsBot: boolean
118
+ browserIsChromeFamily: boolean
119
+ browserIsElectron: boolean
120
+ browserIsFromEU: boolean
121
+ browserIsStandalonePWA: boolean
122
+
123
+ deviceType: typeof DeviceType[keyof typeof DeviceType] | undefined
124
+ deviceVendor:
125
+ | typeof DeviceVendor[keyof typeof DeviceVendor]
126
+ | typeof Extension['DeviceVendor']['Vehicle'][keyof typeof Extension['DeviceVendor']['Vehicle']]
127
+ | undefined
128
+ deviceModel: IResult['device']['model'] | undefined
129
+ deviceIsAppleSilicon: boolean
130
+ }
131
+ /**
132
+ * Parse the user-agent string into normalized device metadata.
133
+ *
134
+ * @see {@link https://docs.uaparser.dev/ | UAParser.js}
135
+ */
136
+ export const parseUserAgent = (userAgent: string): DeviceUserAgentInfo => {
137
+ // @ts-expect-error - UAParser.js type definitions has unknown issue with extensions
138
+ const parser = new UAParser([Bots, CLIs, Crawlers, Emails, ExtraDevices, Fetchers, InApps, Libraries, MediaPlayers, Vehicles])
139
+ parser.setUA(userAgent)
140
+ const result = parser.getResult()
141
+
142
+ let browserIsElectron: boolean
143
+ try {
144
+ browserIsElectron = isElectron()
145
+ } catch {
146
+ browserIsElectron = false
147
+ }
148
+
149
+ let browserIsStandalonePWA: boolean
150
+ try {
151
+ browserIsStandalonePWA = isStandalonePWA()
152
+ } catch {
153
+ browserIsStandalonePWA = false
154
+ }
155
+
156
+ return {
157
+ ua: userAgent,
158
+ uaIsFrozen: isFrozenUA(userAgent),
159
+
160
+ cpuArchitecture: result.cpu.architecture,
161
+
162
+ // oxlint-disable-next-line no-unsafe-type-assertion
163
+ osName: result.os.name as typeof OSName[keyof typeof OSName],
164
+ osVersion: result.os.version,
165
+
166
+ engineName: result.engine.name,
167
+ engineVersion: result.engine.version,
168
+
169
+ // oxlint-disable-next-line no-unsafe-type-assertion
170
+ browserName: result.browser.name as typeof BrowserName[keyof typeof BrowserName],
171
+ browserVersion: result.browser.version,
172
+ browserMajor: result.browser.major,
173
+ browserType: result.browser.type,
174
+ browserIsAiAssistant: isAIAssistant(userAgent),
175
+ browserIsAiCrawler: isAICrawler(userAgent),
176
+ browserIsBot: isBot(userAgent),
177
+ browserIsChromeFamily: isChromeFamily(userAgent),
178
+ browserIsElectron,
179
+ browserIsFromEU: isFromEU(),
180
+ browserIsStandalonePWA,
181
+
182
+ deviceType: result.device.type,
183
+ // oxlint-disable-next-line no-unsafe-type-assertion
184
+ deviceVendor: result.device.vendor as typeof DeviceVendor[keyof typeof DeviceVendor],
185
+ deviceModel: result.device.model,
186
+ deviceIsAppleSilicon: isAppleSilicon(userAgent),
187
+ }
188
+ }
189
+
190
+ const getSafeAreaInset = (edge: "top" | "bottom" | "left" | "right"): number => {
191
+ const style = getComputedStyle(document.documentElement);
192
+
193
+ // 尝试 env(),现代 Safari
194
+ let value = style.getPropertyValue(`env(safe-area-inset-${edge})`);
195
+ if (value === "") {
196
+ // 尝试 constant(),旧版 iOS
197
+ value = style.getPropertyValue(`constant(safe-area-inset-${edge})`);
198
+ }
199
+
200
+ return value !== "" ? Number.parseFloat(value) : 0;
201
+ }
202
+
203
+ /**
204
+ * Describe screen, viewport, and safe-area metadata.
205
+ */
206
+ export interface DeviceScreenInfo {
207
+ pixelRatio: number
208
+ orientationOriginal: OrientationType | 'unknown'
209
+ orientationCalculated: 'portrait' | 'landscape'
210
+
211
+ screenWidth: number
212
+ screenHeight: number
213
+ screenAvailableWidth: number
214
+ screenAvailableHeight: number
215
+ browserWidth: number
216
+ browserHeight: number
217
+ /**
218
+ * With scrollbar.
219
+ */
220
+ viewportWidth: number
221
+ /**
222
+ * With scrollbar.
223
+ */
224
+ viewportHeight: number
225
+ /**
226
+ * Without scrollbar.
227
+ */
228
+ viewportAvailableWidth: number
229
+ /**
230
+ * Without scrollbar.
231
+ */
232
+ viewportAvailableHeight: number
233
+ /**
234
+ * Without scrollbar.
235
+ */
236
+ visualViewportWidth: number | undefined
237
+ /**
238
+ * Without scrollbar.
239
+ */
240
+ visualViewportHeight: number | undefined
241
+ visualViewportOffsetLeft: number | undefined
242
+ visualViewportOffsetTop: number | undefined
243
+ visualViewportPageLeft: number | undefined
244
+ visualViewportPageTop: number | undefined
245
+ visualViewportScale: number | undefined
246
+ safeAreaTop: number
247
+ safeAreaBottom: number
248
+ safeAreaLeft: number
249
+ safeAreaRight: number
250
+ contentWidth: number
251
+ contentHeight: number
252
+ }
253
+ /**
254
+ * Collect screen and viewport information from the current window.
255
+ *
256
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Window/visualViewport}
257
+ */
258
+ export const getDeviceScreenInfo = (): DeviceScreenInfo => {
259
+ return {
260
+ pixelRatio: window.devicePixelRatio,
261
+ orientationOriginal: window.screen.orientation?.type || 'unknown',
262
+ orientationCalculated: window.innerWidth > window.innerHeight ? 'landscape' : 'portrait',
263
+
264
+ screenWidth: window.screen.width,
265
+ screenHeight: window.screen.height,
266
+ screenAvailableWidth: window.screen.availWidth,
267
+ screenAvailableHeight: window.screen.availHeight,
268
+ browserWidth: window.outerWidth,
269
+ browserHeight: window.outerHeight,
270
+ viewportWidth: window.innerWidth,
271
+ viewportHeight: window.innerHeight,
272
+ viewportAvailableWidth: document.documentElement.clientWidth,
273
+ viewportAvailableHeight: document.documentElement.clientHeight,
274
+ visualViewportWidth: window.visualViewport?.width,
275
+ visualViewportHeight: window.visualViewport?.height,
276
+ visualViewportOffsetLeft: window.visualViewport?.offsetLeft,
277
+ visualViewportOffsetTop: window.visualViewport?.offsetTop,
278
+ visualViewportPageLeft: window.visualViewport?.pageLeft,
279
+ visualViewportPageTop: window.visualViewport?.pageTop,
280
+ visualViewportScale: window.visualViewport?.scale,
281
+ safeAreaTop: getSafeAreaInset('top'),
282
+ safeAreaBottom: getSafeAreaInset('bottom'),
283
+ safeAreaLeft: getSafeAreaInset('left'),
284
+ safeAreaRight: getSafeAreaInset('right'),
285
+ contentWidth: document.documentElement.scrollWidth,
286
+ contentHeight: document.documentElement.scrollHeight
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Describe merged device information from navigator, UA, and screen.
292
+ */
293
+ export interface DeviceInfo {
294
+ navigator: DeviceNavigatorInfo
295
+ userAgent: DeviceUserAgentInfo
296
+ screen: DeviceScreenInfo
297
+ }
298
+ /**
299
+ * Get merged device information from navigator, user-agent, and screen.
300
+ */
301
+ export const getDeviceInfo = (userAgent: string): DeviceInfo => {
302
+ const navigatorInfo = getDeviceNavigatorInfo();
303
+ const userAgentInfo = parseUserAgent(userAgent);
304
+ const screenInfo = getDeviceScreenInfo();
305
+
306
+ return {
307
+ navigator: navigatorInfo,
308
+ userAgent: userAgentInfo,
309
+ screen: screenInfo
310
+ };
311
+ }