@yunzai-ng/core 0.1.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.
- package/LICENSE +661 -0
- package/README.md +51 -0
- package/dist/adapter/accounts.d.ts +174 -0
- package/dist/adapter/accounts.d.ts.map +1 -0
- package/dist/adapter/accounts.js +653 -0
- package/dist/adapter/accounts.js.map +1 -0
- package/dist/adapter/bots.d.ts +142 -0
- package/dist/adapter/bots.d.ts.map +1 -0
- package/dist/adapter/bots.js +454 -0
- package/dist/adapter/bots.js.map +1 -0
- package/dist/adapter/host.d.ts +114 -0
- package/dist/adapter/host.d.ts.map +1 -0
- package/dist/adapter/host.js +90 -0
- package/dist/adapter/host.js.map +1 -0
- package/dist/adapter/login.d.ts +174 -0
- package/dist/adapter/login.d.ts.map +1 -0
- package/dist/adapter/login.js +345 -0
- package/dist/adapter/login.js.map +1 -0
- package/dist/adapter/registry.d.ts +102 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/registry.js +150 -0
- package/dist/adapter/registry.js.map +1 -0
- package/dist/config/core-config.d.ts +151 -0
- package/dist/config/core-config.d.ts.map +1 -0
- package/dist/config/core-config.js +338 -0
- package/dist/config/core-config.js.map +1 -0
- package/dist/config/schema.d.ts +449 -0
- package/dist/config/schema.d.ts.map +1 -0
- package/dist/config/schema.js +871 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/config/store.d.ts +184 -0
- package/dist/config/store.d.ts.map +1 -0
- package/dist/config/store.js +428 -0
- package/dist/config/store.js.map +1 -0
- package/dist/config/yaml.d.ts +23 -0
- package/dist/config/yaml.d.ts.map +1 -0
- package/dist/config/yaml.js +108 -0
- package/dist/config/yaml.js.map +1 -0
- package/dist/http/client.d.ts +82 -0
- package/dist/http/client.d.ts.map +1 -0
- package/dist/http/client.js +658 -0
- package/dist/http/client.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -0
- package/dist/index.js.map +1 -0
- package/dist/kernel/app.d.ts +244 -0
- package/dist/kernel/app.d.ts.map +1 -0
- package/dist/kernel/app.js +654 -0
- package/dist/kernel/app.js.map +1 -0
- package/dist/kernel/kv-sink.d.ts +34 -0
- package/dist/kernel/kv-sink.d.ts.map +1 -0
- package/dist/kernel/kv-sink.js +25 -0
- package/dist/kernel/kv-sink.js.map +1 -0
- package/dist/kernel/policy.d.ts +77 -0
- package/dist/kernel/policy.d.ts.map +1 -0
- package/dist/kernel/policy.js +99 -0
- package/dist/kernel/policy.js.map +1 -0
- package/dist/kernel/runtime.d.ts +117 -0
- package/dist/kernel/runtime.d.ts.map +1 -0
- package/dist/kernel/runtime.js +197 -0
- package/dist/kernel/runtime.js.map +1 -0
- package/dist/kernel/sql-sink.d.ts +36 -0
- package/dist/kernel/sql-sink.d.ts.map +1 -0
- package/dist/kernel/sql-sink.js +114 -0
- package/dist/kernel/sql-sink.js.map +1 -0
- package/dist/logger/format.d.ts +67 -0
- package/dist/logger/format.d.ts.map +1 -0
- package/dist/logger/format.js +218 -0
- package/dist/logger/format.js.map +1 -0
- package/dist/logger/index.d.ts +97 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +363 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/rotate.d.ts +47 -0
- package/dist/logger/rotate.d.ts.map +1 -0
- package/dist/logger/rotate.js +242 -0
- package/dist/logger/rotate.js.map +1 -0
- package/dist/message/segment.d.ts +255 -0
- package/dist/message/segment.d.ts.map +1 -0
- package/dist/message/segment.js +510 -0
- package/dist/message/segment.js.map +1 -0
- package/dist/message/split.d.ts +34 -0
- package/dist/message/split.d.ts.map +1 -0
- package/dist/message/split.js +79 -0
- package/dist/message/split.js.map +1 -0
- package/dist/message/target.d.ts +29 -0
- package/dist/message/target.d.ts.map +1 -0
- package/dist/message/target.js +34 -0
- package/dist/message/target.js.map +1 -0
- package/dist/pipeline/cooldown.d.ts +84 -0
- package/dist/pipeline/cooldown.d.ts.map +1 -0
- package/dist/pipeline/cooldown.js +94 -0
- package/dist/pipeline/cooldown.js.map +1 -0
- package/dist/pipeline/dispatch.d.ts +98 -0
- package/dist/pipeline/dispatch.d.ts.map +1 -0
- package/dist/pipeline/dispatch.js +323 -0
- package/dist/pipeline/dispatch.js.map +1 -0
- package/dist/pipeline/event.d.ts +130 -0
- package/dist/pipeline/event.d.ts.map +1 -0
- package/dist/pipeline/event.js +538 -0
- package/dist/pipeline/event.js.map +1 -0
- package/dist/pipeline/middleware.d.ts +63 -0
- package/dist/pipeline/middleware.d.ts.map +1 -0
- package/dist/pipeline/middleware.js +174 -0
- package/dist/pipeline/middleware.js.map +1 -0
- package/dist/pipeline/prompt.d.ts +80 -0
- package/dist/pipeline/prompt.d.ts.map +1 -0
- package/dist/pipeline/prompt.js +155 -0
- package/dist/pipeline/prompt.js.map +1 -0
- package/dist/pipeline/router.d.ts +98 -0
- package/dist/pipeline/router.d.ts.map +1 -0
- package/dist/pipeline/router.js +489 -0
- package/dist/pipeline/router.js.map +1 -0
- package/dist/platform/detect.d.ts +24 -0
- package/dist/platform/detect.d.ts.map +1 -0
- package/dist/platform/detect.js +178 -0
- package/dist/platform/detect.js.map +1 -0
- package/dist/platform/paths.d.ts +38 -0
- package/dist/platform/paths.d.ts.map +1 -0
- package/dist/platform/paths.js +129 -0
- package/dist/platform/paths.js.map +1 -0
- package/dist/platform/system.d.ts +71 -0
- package/dist/platform/system.d.ts.map +1 -0
- package/dist/platform/system.js +242 -0
- package/dist/platform/system.js.map +1 -0
- package/dist/plugin/context.d.ts +92 -0
- package/dist/plugin/context.d.ts.map +1 -0
- package/dist/plugin/context.js +573 -0
- package/dist/plugin/context.js.map +1 -0
- package/dist/plugin/define.d.ts +92 -0
- package/dist/plugin/define.d.ts.map +1 -0
- package/dist/plugin/define.js +82 -0
- package/dist/plugin/define.js.map +1 -0
- package/dist/plugin/discover.d.ts +106 -0
- package/dist/plugin/discover.d.ts.map +1 -0
- package/dist/plugin/discover.js +254 -0
- package/dist/plugin/discover.js.map +1 -0
- package/dist/plugin/events.d.ts +109 -0
- package/dist/plugin/events.d.ts.map +1 -0
- package/dist/plugin/events.js +209 -0
- package/dist/plugin/events.js.map +1 -0
- package/dist/plugin/hooks.d.ts +266 -0
- package/dist/plugin/hooks.d.ts.map +1 -0
- package/dist/plugin/hooks.js +84 -0
- package/dist/plugin/hooks.js.map +1 -0
- package/dist/plugin/host.d.ts +160 -0
- package/dist/plugin/host.d.ts.map +1 -0
- package/dist/plugin/host.js +546 -0
- package/dist/plugin/host.js.map +1 -0
- package/dist/plugin/market.d.ts +264 -0
- package/dist/plugin/market.d.ts.map +1 -0
- package/dist/plugin/market.js +700 -0
- package/dist/plugin/market.js.map +1 -0
- package/dist/plugin/services.d.ts +116 -0
- package/dist/plugin/services.d.ts.map +1 -0
- package/dist/plugin/services.js +213 -0
- package/dist/plugin/services.js.map +1 -0
- package/dist/plugin/tar.d.ts +65 -0
- package/dist/plugin/tar.d.ts.map +1 -0
- package/dist/plugin/tar.js +277 -0
- package/dist/plugin/tar.js.map +1 -0
- package/dist/render/registry.d.ts +127 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +306 -0
- package/dist/render/registry.js.map +1 -0
- package/dist/scheduler/index.d.ts +67 -0
- package/dist/scheduler/index.d.ts.map +1 -0
- package/dist/scheduler/index.js +279 -0
- package/dist/scheduler/index.js.map +1 -0
- package/dist/server/api.d.ts +179 -0
- package/dist/server/api.d.ts.map +1 -0
- package/dist/server/api.js +602 -0
- package/dist/server/api.js.map +1 -0
- package/dist/server/auth.d.ts +83 -0
- package/dist/server/auth.d.ts.map +1 -0
- package/dist/server/auth.js +178 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/browse.d.ts +60 -0
- package/dist/server/browse.d.ts.map +1 -0
- package/dist/server/browse.js +187 -0
- package/dist/server/browse.js.map +1 -0
- package/dist/server/files.d.ts +27 -0
- package/dist/server/files.d.ts.map +1 -0
- package/dist/server/files.js +76 -0
- package/dist/server/files.js.map +1 -0
- package/dist/server/index.d.ts +189 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +1123 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/table.d.ts +105 -0
- package/dist/server/table.d.ts.map +1 -0
- package/dist/server/table.js +289 -0
- package/dist/server/table.js.map +1 -0
- package/dist/store/index.d.ts +50 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +120 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/json.d.ts +73 -0
- package/dist/store/json.d.ts.map +1 -0
- package/dist/store/json.js +174 -0
- package/dist/store/json.js.map +1 -0
- package/dist/store/kv.d.ts +126 -0
- package/dist/store/kv.d.ts.map +1 -0
- package/dist/store/kv.js +232 -0
- package/dist/store/kv.js.map +1 -0
- package/dist/store/level.d.ts +103 -0
- package/dist/store/level.d.ts.map +1 -0
- package/dist/store/level.js +119 -0
- package/dist/store/level.js.map +1 -0
- package/dist/store/memory.d.ts +61 -0
- package/dist/store/memory.d.ts.map +1 -0
- package/dist/store/memory.js +94 -0
- package/dist/store/memory.js.map +1 -0
- package/dist/store/sql.d.ts +142 -0
- package/dist/store/sql.d.ts.map +1 -0
- package/dist/store/sql.js +246 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/testing/fake.d.ts +109 -0
- package/dist/testing/fake.d.ts.map +1 -0
- package/dist/testing/fake.js +196 -0
- package/dist/testing/fake.js.map +1 -0
- package/dist/testing/index.d.ts +16 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +16 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-adapter.d.ts +205 -0
- package/dist/testing/mock-adapter.d.ts.map +1 -0
- package/dist/testing/mock-adapter.js +309 -0
- package/dist/testing/mock-adapter.js.map +1 -0
- package/dist/util/deep.d.ts +108 -0
- package/dist/util/deep.d.ts.map +1 -0
- package/dist/util/deep.js +275 -0
- package/dist/util/deep.js.map +1 -0
- package/dist/util/defer.d.ts +107 -0
- package/dist/util/defer.d.ts.map +1 -0
- package/dist/util/defer.js +163 -0
- package/dist/util/defer.js.map +1 -0
- package/dist/util/dispose.d.ts +88 -0
- package/dist/util/dispose.d.ts.map +1 -0
- package/dist/util/dispose.js +143 -0
- package/dist/util/dispose.js.map +1 -0
- package/dist/util/duration.d.ts +28 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +77 -0
- package/dist/util/duration.js.map +1 -0
- package/dist/util/fs.d.ts +110 -0
- package/dist/util/fs.d.ts.map +1 -0
- package/dist/util/fs.js +269 -0
- package/dist/util/fs.js.map +1 -0
- package/dist/util/id.d.ts +49 -0
- package/dist/util/id.d.ts.map +1 -0
- package/dist/util/id.js +87 -0
- package/dist/util/id.js.map +1 -0
- package/dist/util/lru.d.ts +74 -0
- package/dist/util/lru.d.ts.map +1 -0
- package/dist/util/lru.js +156 -0
- package/dist/util/lru.js.map +1 -0
- package/dist/util/queue.d.ts +64 -0
- package/dist/util/queue.d.ts.map +1 -0
- package/dist/util/queue.js +147 -0
- package/dist/util/queue.js.map +1 -0
- package/dist/util/text.d.ts +86 -0
- package/dist/util/text.d.ts.map +1 -0
- package/dist/util/text.js +182 -0
- package/dist/util/text.js.map +1 -0
- package/package.json +44 -0
- package/src/adapter/accounts.ts +758 -0
- package/src/adapter/bots.ts +582 -0
- package/src/adapter/host.ts +224 -0
- package/src/adapter/login.ts +481 -0
- package/src/adapter/registry.ts +202 -0
- package/src/config/core-config.test.ts +60 -0
- package/src/config/core-config.ts +401 -0
- package/src/config/schema.test.ts +179 -0
- package/src/config/schema.ts +1036 -0
- package/src/config/store.test.ts +245 -0
- package/src/config/store.ts +506 -0
- package/src/config/yaml.ts +119 -0
- package/src/http/client.test.ts +568 -0
- package/src/http/client.ts +777 -0
- package/src/index.ts +106 -0
- package/src/kernel/app.test.ts +267 -0
- package/src/kernel/app.ts +843 -0
- package/src/kernel/kv-sink.ts +58 -0
- package/src/kernel/policy.ts +120 -0
- package/src/kernel/runtime.test.ts +542 -0
- package/src/kernel/runtime.ts +307 -0
- package/src/kernel/sql-sink.ts +154 -0
- package/src/logger/format.ts +259 -0
- package/src/logger/index.ts +427 -0
- package/src/logger/rotate.ts +265 -0
- package/src/message/segment.test.ts +335 -0
- package/src/message/segment.ts +569 -0
- package/src/message/split.ts +97 -0
- package/src/message/target.ts +48 -0
- package/src/pipeline/cooldown.ts +136 -0
- package/src/pipeline/dispatch.ts +419 -0
- package/src/pipeline/event.ts +735 -0
- package/src/pipeline/middleware.test.ts +426 -0
- package/src/pipeline/middleware.ts +199 -0
- package/src/pipeline/prompt.ts +212 -0
- package/src/pipeline/router.test.ts +432 -0
- package/src/pipeline/router.ts +575 -0
- package/src/platform/detect.ts +178 -0
- package/src/platform/paths.ts +147 -0
- package/src/platform/system.test.ts +102 -0
- package/src/platform/system.ts +289 -0
- package/src/plugin/context.test.ts +459 -0
- package/src/plugin/context.ts +757 -0
- package/src/plugin/define.test.ts +120 -0
- package/src/plugin/define.ts +142 -0
- package/src/plugin/discover.test.ts +253 -0
- package/src/plugin/discover.ts +353 -0
- package/src/plugin/events.test.ts +162 -0
- package/src/plugin/events.ts +266 -0
- package/src/plugin/hooks.ts +373 -0
- package/src/plugin/host.test.ts +696 -0
- package/src/plugin/host.ts +738 -0
- package/src/plugin/market.test.ts +781 -0
- package/src/plugin/market.ts +870 -0
- package/src/plugin/services.test.ts +135 -0
- package/src/plugin/services.ts +261 -0
- package/src/plugin/tar.test.ts +158 -0
- package/src/plugin/tar.ts +309 -0
- package/src/render/registry.test.ts +127 -0
- package/src/render/registry.ts +439 -0
- package/src/scheduler/index.ts +357 -0
- package/src/server/api.test.ts +802 -0
- package/src/server/api.ts +869 -0
- package/src/server/auth.ts +201 -0
- package/src/server/browse.test.ts +193 -0
- package/src/server/browse.ts +240 -0
- package/src/server/files.ts +97 -0
- package/src/server/index.test.ts +566 -0
- package/src/server/index.ts +1386 -0
- package/src/server/table.ts +351 -0
- package/src/store/index.ts +156 -0
- package/src/store/json.ts +198 -0
- package/src/store/kv.test.ts +307 -0
- package/src/store/kv.ts +256 -0
- package/src/store/level.ts +167 -0
- package/src/store/memory.ts +109 -0
- package/src/store/sql.test.ts +204 -0
- package/src/store/sql.ts +324 -0
- package/src/testing/fake.ts +297 -0
- package/src/testing/index.ts +15 -0
- package/src/testing/mock-adapter.ts +588 -0
- package/src/util/deep.ts +272 -0
- package/src/util/defer.ts +211 -0
- package/src/util/dispose.ts +169 -0
- package/src/util/duration.ts +82 -0
- package/src/util/fs.ts +279 -0
- package/src/util/id.ts +94 -0
- package/src/util/lru.ts +185 -0
- package/src/util/queue.ts +167 -0
- package/src/util/text.ts +189 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`definePlugin()` 与两个判定函数的测试
|
|
3
|
+
* 依赖方向:测试文件
|
|
4
|
+
* 生命周期:纯函数,无需清理
|
|
5
|
+
* 注意事项:插件名会被同时用作**配置文件名、KV 命名空间、URL 前缀、日志 scope**,
|
|
6
|
+
* 因此"非法名称必须在 import 的那一刻即抛错"是此处的核心断言 ——
|
|
7
|
+
* 若予放行,症状将表现为写入配置时一个无从解释的 ENOENT,或更为严重的情形:
|
|
8
|
+
* 一个插件读写到了其他插件的 KV 命名空间。
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest"
|
|
11
|
+
import { definePlugin, isPluginDefinition, looksLikePlugin } from "./define.js"
|
|
12
|
+
import { s } from "../config/schema.js"
|
|
13
|
+
|
|
14
|
+
/** 品牌标记用的注册表符号(插件与内核可能来自不同模块实例,故用 Symbol.for) */
|
|
15
|
+
const BRAND = Symbol.for("yunzai-ng.plugin")
|
|
16
|
+
|
|
17
|
+
describe("definePlugin", () => {
|
|
18
|
+
it("返回带品牌标记的定义", () => {
|
|
19
|
+
const def = definePlugin({ name: "demo", setup: () => undefined })
|
|
20
|
+
|
|
21
|
+
expect(def.name).toBe("demo")
|
|
22
|
+
expect(isPluginDefinition(def)).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("原样保留元信息与 schema", () => {
|
|
26
|
+
const schema = s.object({ cookie: s.string().default("") })
|
|
27
|
+
const def = definePlugin({
|
|
28
|
+
name: "mhy-game",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
description: "米哈游游戏插件",
|
|
31
|
+
author: "someone",
|
|
32
|
+
dependencies: ["renderer-puppeteer"],
|
|
33
|
+
priority: 50,
|
|
34
|
+
configSchema: schema,
|
|
35
|
+
setup: () => undefined
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
expect(def).toMatchObject({
|
|
39
|
+
name: "mhy-game",
|
|
40
|
+
version: "1.0.0",
|
|
41
|
+
description: "米哈游游戏插件",
|
|
42
|
+
dependencies: ["renderer-puppeteer"],
|
|
43
|
+
priority: 50
|
|
44
|
+
})
|
|
45
|
+
expect(def.configSchema).toBe(schema)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it("冻结定义,插件改不动自己的元信息", () => {
|
|
49
|
+
const def = definePlugin({ name: "demo", setup: () => undefined })
|
|
50
|
+
// 改了内核也不会重新读,只会让人以为改生效了
|
|
51
|
+
expect(() => {
|
|
52
|
+
;(def as { name: string }).name = "别的名字"
|
|
53
|
+
}).toThrow()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it("缺 name 就抛错", () => {
|
|
57
|
+
expect(() => definePlugin({ setup: () => undefined } as never)).toThrow(/缺少 name/)
|
|
58
|
+
expect(() => definePlugin({ name: "", setup: () => undefined })).toThrow(/缺少 name/)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("非法插件名抛错并说明规则", () => {
|
|
62
|
+
for (const name of ["1abc", "有中文", "with space", "a:b", "a/b", "-lead", "_lead"]) {
|
|
63
|
+
expect(() => definePlugin({ name, setup: () => undefined }), name).toThrow(/不合法/)
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("合法插件名放行", () => {
|
|
68
|
+
for (const name of ["demo", "mhy-game-plugin", "adapter.napcat", "A_b-1", "x"]) {
|
|
69
|
+
expect(() => definePlugin({ name, setup: () => undefined }), name).not.toThrow()
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("缺 setup 抛错", () => {
|
|
74
|
+
expect(() => definePlugin({ name: "demo" } as never)).toThrow(/缺少 setup/)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it("dependencies 写法错误抛错", () => {
|
|
78
|
+
expect(() => definePlugin({ name: "demo", dependencies: "other" as never, setup: () => undefined })).toThrow(
|
|
79
|
+
/必须是字符串数组/
|
|
80
|
+
)
|
|
81
|
+
expect(() => definePlugin({ name: "demo", dependencies: [""], setup: () => undefined })).toThrow(/含空项/)
|
|
82
|
+
expect(() => definePlugin({ name: "demo", dependencies: ["demo"], setup: () => undefined })).toThrow(/依赖了自己/)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("priority 必须是数字", () => {
|
|
86
|
+
expect(() => definePlugin({ name: "demo", priority: Number.NaN, setup: () => undefined })).toThrow(
|
|
87
|
+
/priority 必须是数字/
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("isPluginDefinition", () => {
|
|
93
|
+
it("只认 definePlugin 的产物", () => {
|
|
94
|
+
expect(isPluginDefinition({ name: "demo", setup: () => undefined })).toBe(false)
|
|
95
|
+
expect(isPluginDefinition(undefined)).toBe(false)
|
|
96
|
+
expect(isPluginDefinition(null)).toBe(false)
|
|
97
|
+
expect(isPluginDefinition("demo")).toBe(false)
|
|
98
|
+
expect(isPluginDefinition(() => undefined)).toBe(false)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it("品牌用注册表符号,跨模块实例也认得", () => {
|
|
102
|
+
// 插件可能链接了另一份 @yunzai-ng/core(版本不同的两个 node_modules 副本)。
|
|
103
|
+
// Symbol.for 是全进程共享的,所以这种情况下判定仍然成立 ——
|
|
104
|
+
// 换成模块内的 Symbol() 就会退化成"看起来像插件"的宽松路径。
|
|
105
|
+
expect(isPluginDefinition({ name: "demo", setup: () => undefined, [BRAND]: true })).toBe(true)
|
|
106
|
+
})
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
describe("looksLikePlugin", () => {
|
|
110
|
+
it("有 name 与 setup 即可(手写定义、compat 插件动态生成的定义)", () => {
|
|
111
|
+
expect(looksLikePlugin({ name: "demo", setup: () => undefined })).toBe(true)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("缺一个就不算", () => {
|
|
115
|
+
expect(looksLikePlugin({ name: "demo" })).toBe(false)
|
|
116
|
+
expect(looksLikePlugin({ setup: () => undefined })).toBe(false)
|
|
117
|
+
expect(looksLikePlugin({ name: 1, setup: () => undefined })).toBe(false)
|
|
118
|
+
expect(looksLikePlugin(null)).toBe(false)
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:`definePlugin()` —— 插件的声明入口与配置类型推导
|
|
3
|
+
* 依赖方向:依赖 config/schema、类型包
|
|
4
|
+
* 生命周期:模块加载期执行一次(纯校验,无副作用)
|
|
5
|
+
* 注意事项:两个设计要点 ——
|
|
6
|
+
*
|
|
7
|
+
* 1) **配置类型自动推导**。`configSchema: s.object({ cookie: s.string() })`
|
|
8
|
+
* 之后,`ctx.config.get().cookie` 就是 `string`,写错字段名编译期报错。
|
|
9
|
+
* 类型包为保持叶子(不许依赖任何工作区包)故把 `configSchema` 声明为
|
|
10
|
+
* `unknown`,推导只能在这里做 —— 这也是插件必须从 `@yunzai-ng/core`
|
|
11
|
+
* 导入 `definePlugin` 而不是自己拼对象的原因。
|
|
12
|
+
*
|
|
13
|
+
* 2) **在声明期就校验元信息**。插件名会被当作配置文件名、KV 命名空间、
|
|
14
|
+
* URL 前缀和日志 scope 使用,非法名字必须在 `import` 的那一刻就报错,
|
|
15
|
+
* 而不是等到写文件时抛一个看不出所以然的 ENOENT。
|
|
16
|
+
*/
|
|
17
|
+
import type { Awaitable, Disposer, PluginContext, PluginDefinition, PluginMeta } from "@yunzai-ng/types"
|
|
18
|
+
import type { Schema } from "../config/schema.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 插件名合法形式
|
|
22
|
+
*
|
|
23
|
+
* 与配置名规则保持一致(见 config/store.ts):插件名直接就是
|
|
24
|
+
* `config/<name>.yaml` 的文件名。额外排除了 `:`(KV 命名空间分隔符)
|
|
25
|
+
* 与路径分隔符,否则一个插件能读写别人的命名空间。
|
|
26
|
+
*/
|
|
27
|
+
const NAME_RE = /^[a-z][a-z0-9._-]*$/i
|
|
28
|
+
|
|
29
|
+
/** 标记"这是 definePlugin 的产物",用于加载时校验默认导出 */
|
|
30
|
+
const BRAND = Symbol.for("yunzai-ng.plugin")
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 从 schema 推出配置类型
|
|
34
|
+
*
|
|
35
|
+
* 未声明 schema 时给出 `Record<string, never>`:`ctx.config.get()` 仍可调用,
|
|
36
|
+
* 但读取任何字段均会在编译期报错,以提示作者"尚未声明配置"。
|
|
37
|
+
*/
|
|
38
|
+
export type ConfigOf<S> = S extends Schema<infer T> ? T : Record<string, never>
|
|
39
|
+
|
|
40
|
+
/** `definePlugin` 的入参 */
|
|
41
|
+
export interface DefinePluginInput<S extends Schema<unknown> | undefined = undefined> extends PluginMeta {
|
|
42
|
+
/**
|
|
43
|
+
* 配置 schema
|
|
44
|
+
*
|
|
45
|
+
* 用 `s.object({...})` 构造。内核据此生成带中文注释的 YAML、
|
|
46
|
+
* 渲染 WebUI 表单、校验用户输入 —— 三处共用一份声明,永不脱节。
|
|
47
|
+
*/
|
|
48
|
+
configSchema?: S
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 插件入口
|
|
52
|
+
*
|
|
53
|
+
* 在这里注册命令、中间件、定时任务等。**不要**在这里做耗时的网络请求:
|
|
54
|
+
* 内核对 setup 有超时(默认 30s),超时会把插件标记为加载失败。
|
|
55
|
+
* 需要预热的资源请注册 `ctx.on("app/ready", ...)` 或用 `ctx.cron`。
|
|
56
|
+
* @param ctx 注入的上下文
|
|
57
|
+
* @returns 可选的清理函数,等价于在里面调 `ctx.onDispose`
|
|
58
|
+
*/
|
|
59
|
+
setup(ctx: PluginContext<ConfigOf<S>>): Awaitable<void | Disposer>
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 带品牌标记的插件定义(内核内部使用) */
|
|
63
|
+
export interface BrandedPluginDefinition<C = unknown> extends PluginDefinition<C> {
|
|
64
|
+
/** 品牌标记 */
|
|
65
|
+
readonly [BRAND]: true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 声明一个插件
|
|
70
|
+
*
|
|
71
|
+
* @param input 插件定义
|
|
72
|
+
* @returns 供内核加载的插件定义(已冻结)
|
|
73
|
+
* @throws 插件名缺失/非法、`setup` 不是函数、依赖项写法错误时
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* export default definePlugin({
|
|
77
|
+
* name: "mhy-game",
|
|
78
|
+
* configSchema: s.object({ cookie: s.string().default("") }),
|
|
79
|
+
* setup(ctx) {
|
|
80
|
+
* ctx.command("#体力").desc("查询实时便笺").action(async e => {
|
|
81
|
+
* await e.reply(ctx.config.get().cookie ? "查询中" : "请先配置 cookie")
|
|
82
|
+
* })
|
|
83
|
+
* }
|
|
84
|
+
* })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function definePlugin<S extends Schema<unknown> | undefined = undefined>(
|
|
88
|
+
input: DefinePluginInput<S>
|
|
89
|
+
): PluginDefinition<ConfigOf<S>> {
|
|
90
|
+
if (typeof input?.name !== "string" || input.name.length === 0) {
|
|
91
|
+
throw new Error("definePlugin:缺少 name,插件必须有名字")
|
|
92
|
+
}
|
|
93
|
+
if (!NAME_RE.test(input.name)) {
|
|
94
|
+
throw new Error(`definePlugin:插件名 ${input.name} 不合法。需以字母开头,只含字母、数字与 . - _(它同时是配置文件名与存储命名空间)`)
|
|
95
|
+
}
|
|
96
|
+
if (typeof input.setup !== "function") {
|
|
97
|
+
throw new Error(`definePlugin:插件 ${input.name} 缺少 setup 函数`)
|
|
98
|
+
}
|
|
99
|
+
if (input.dependencies && !Array.isArray(input.dependencies)) {
|
|
100
|
+
throw new Error(`definePlugin:插件 ${input.name} 的 dependencies 必须是字符串数组`)
|
|
101
|
+
}
|
|
102
|
+
for (const dep of input.dependencies ?? []) {
|
|
103
|
+
if (typeof dep !== "string" || dep.length === 0) {
|
|
104
|
+
throw new Error(`definePlugin:插件 ${input.name} 的 dependencies 含空项`)
|
|
105
|
+
}
|
|
106
|
+
if (dep === input.name) {
|
|
107
|
+
throw new Error(`definePlugin:插件 ${input.name} 依赖了自己`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (input.priority !== undefined && !Number.isFinite(input.priority)) {
|
|
111
|
+
throw new Error(`definePlugin:插件 ${input.name} 的 priority 必须是数字`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 浅冻结:防止插件在运行期改自己的元信息(改了内核也不会重新读,只会造成困惑)
|
|
115
|
+
return Object.freeze({ ...input, [BRAND]: true }) as unknown as PluginDefinition<ConfigOf<S>>
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 判断一个值是否 `definePlugin` 的产物
|
|
120
|
+
*
|
|
121
|
+
* 加载器用它区分"插件写错了默认导出"和"插件确实是插件",
|
|
122
|
+
* 从而给出"请用 definePlugin 包一层"这种能直接照做的提示。
|
|
123
|
+
* @param value 待判定值
|
|
124
|
+
* @returns 是否为插件定义
|
|
125
|
+
*/
|
|
126
|
+
export function isPluginDefinition(value: unknown): value is BrandedPluginDefinition {
|
|
127
|
+
return typeof value === "object" && value !== null && (value as Record<symbol, unknown>)[BRAND] === true
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 宽松判定:像插件定义但可能没走 `definePlugin`
|
|
132
|
+
*
|
|
133
|
+
* 允许手写对象(如测试里的假插件、compat 插件动态生成的定义),
|
|
134
|
+
* 只要有 name 与 setup 就接受,但加载器会记一条 debug 提示。
|
|
135
|
+
* @param value 待判定值
|
|
136
|
+
* @returns 是否可当作插件定义使用
|
|
137
|
+
*/
|
|
138
|
+
export function looksLikePlugin(value: unknown): value is PluginDefinition {
|
|
139
|
+
if (typeof value !== "object" || value === null) return false
|
|
140
|
+
const obj = value as Partial<PluginDefinition>
|
|
141
|
+
return typeof obj.name === "string" && typeof obj.setup === "function"
|
|
142
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 模块职责:插件发现与加载顺序规划的测试
|
|
3
|
+
* 依赖方向:测试文件,依赖 plugin/discover、testing/fake
|
|
4
|
+
* 生命周期:每个用例一个临时插件目录
|
|
5
|
+
* 注意事项:`resolveLoadOrder` 的**确定性**是重点断言对象 —— 同一份插件集合在任何机器上
|
|
6
|
+
* 都须得到相同顺序,否则"甲机器正常、乙机器异常"会耗费大量排障时间。
|
|
7
|
+
*/
|
|
8
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
9
|
+
import { tmpdir } from "node:os"
|
|
10
|
+
import { join } from "node:path"
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
12
|
+
import { discoverPlugins, resolveLoadOrder } from "./discover.js"
|
|
13
|
+
import { fakeLogger } from "../testing/fake.js"
|
|
14
|
+
|
|
15
|
+
/** 造一个插件目录 */
|
|
16
|
+
async function makePlugin(
|
|
17
|
+
root: string,
|
|
18
|
+
name: string,
|
|
19
|
+
opts: { pkg?: Record<string, unknown>; entry?: string; content?: string } = {}
|
|
20
|
+
): Promise<string> {
|
|
21
|
+
const dir = join(root, name)
|
|
22
|
+
await mkdir(dir, { recursive: true })
|
|
23
|
+
if (opts.pkg) await writeFile(join(dir, "package.json"), JSON.stringify(opts.pkg))
|
|
24
|
+
const entry = opts.entry ?? "index.js"
|
|
25
|
+
if (entry) {
|
|
26
|
+
await mkdir(join(dir, entry.includes("/") ? entry.split("/").slice(0, -1).join("/") : "."), { recursive: true })
|
|
27
|
+
await writeFile(join(dir, entry), opts.content ?? "export default {}\n")
|
|
28
|
+
}
|
|
29
|
+
return dir
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("discoverPlugins", () => {
|
|
33
|
+
let root: string
|
|
34
|
+
|
|
35
|
+
beforeEach(async () => {
|
|
36
|
+
root = await mkdtemp(join(tmpdir(), "yzng-discover-"))
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
afterEach(async () => {
|
|
40
|
+
await rm(root, { recursive: true, force: true })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("找到带 index.js 的插件目录", async () => {
|
|
44
|
+
await makePlugin(root, "demo")
|
|
45
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
46
|
+
|
|
47
|
+
expect(found).toHaveLength(1)
|
|
48
|
+
expect(found[0]?.id).toBe("demo")
|
|
49
|
+
expect(found[0]?.entry).toBe(join(root, "demo", "index.js"))
|
|
50
|
+
expect(found[0]?.builtin).toBe(false)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("优先用 package.json 声明的入口", async () => {
|
|
54
|
+
await makePlugin(root, "custom", { pkg: { name: "custom", main: "lib/boot.js" }, entry: "lib/boot.js" })
|
|
55
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
56
|
+
expect(found[0]?.entry).toBe(join(root, "custom", "lib", "boot.js"))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("认 exports 的条件导出", async () => {
|
|
60
|
+
await makePlugin(root, "exp", {
|
|
61
|
+
pkg: { name: "exp", exports: { ".": { import: "./dist/mod.js" } } },
|
|
62
|
+
entry: "dist/mod.js"
|
|
63
|
+
})
|
|
64
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
65
|
+
expect(found[0]?.entry).toBe(join(root, "exp", "dist", "mod.js"))
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("scope 包名去掉 scope 作为名字线索", async () => {
|
|
69
|
+
await makePlugin(root, "scoped", { pkg: { name: "@acme/cool-plugin" } })
|
|
70
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
71
|
+
expect(found[0]?.id).toBe("cool-plugin")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it("yunzai.name 优先于包名与目录名", async () => {
|
|
75
|
+
await makePlugin(root, "dirname", { pkg: { name: "pkgname", yunzai: { name: "declared" } } })
|
|
76
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
77
|
+
expect(found[0]?.id).toBe("declared")
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("跳过隐藏目录、下划线开头目录与 node_modules", async () => {
|
|
81
|
+
await makePlugin(root, ".hidden")
|
|
82
|
+
await makePlugin(root, "_disabled")
|
|
83
|
+
await makePlugin(root, "node_modules")
|
|
84
|
+
await makePlugin(root, "real")
|
|
85
|
+
|
|
86
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
87
|
+
expect(found.map(f => f.id)).toEqual(["real"])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("package.json 自标记 disabled 的插件不加载", async () => {
|
|
91
|
+
await makePlugin(root, "off", { pkg: { name: "off", yunzai: { disabled: true } } })
|
|
92
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
93
|
+
expect(found).toHaveLength(0)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("找不到入口时给出照着能解决的警告,而不是静默跳过", async () => {
|
|
97
|
+
const dir = join(root, "broken")
|
|
98
|
+
await mkdir(dir, { recursive: true })
|
|
99
|
+
await writeFile(join(dir, "readme.md"), "# 忘了写 index.js")
|
|
100
|
+
|
|
101
|
+
const logger = fakeLogger()
|
|
102
|
+
const found = await discoverPlugins({ dirs: [root], logger })
|
|
103
|
+
|
|
104
|
+
expect(found).toHaveLength(0)
|
|
105
|
+
const line = logger.lines.find(l => l.includes("broken"))
|
|
106
|
+
expect(line).toBeDefined()
|
|
107
|
+
expect(line).toContain("index.js")
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it("支持顶层单文件插件", async () => {
|
|
111
|
+
await writeFile(join(root, "小功能.js"), "export default {}\n")
|
|
112
|
+
await writeFile(join(root, "_草稿.js"), "export default {}\n")
|
|
113
|
+
|
|
114
|
+
const found = await discoverPlugins({ dirs: [root], logger: fakeLogger() })
|
|
115
|
+
expect(found.map(f => f.id)).toEqual(["小功能"])
|
|
116
|
+
expect(found[0]?.dir).toBe(root)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("同名插件先扫到的胜出,并给出警告", async () => {
|
|
120
|
+
const userDir = await mkdtemp(join(tmpdir(), "yzng-user-"))
|
|
121
|
+
try {
|
|
122
|
+
await makePlugin(userDir, "dup")
|
|
123
|
+
await makePlugin(root, "dup")
|
|
124
|
+
|
|
125
|
+
const logger = fakeLogger()
|
|
126
|
+
// 用户目录排在前面 —— 复制一份预置插件加以修改即应覆盖原插件
|
|
127
|
+
const found = await discoverPlugins({ dirs: [userDir, root], builtinDirs: [root], logger })
|
|
128
|
+
|
|
129
|
+
expect(found).toHaveLength(1)
|
|
130
|
+
expect(found[0]?.dir).toBe(join(userDir, "dup"))
|
|
131
|
+
expect(logger.lines.some(l => l.includes("同名插件"))).toBe(true)
|
|
132
|
+
} finally {
|
|
133
|
+
await rm(userDir, { recursive: true, force: true })
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it("给预置目录里的插件打 builtin 标记", async () => {
|
|
138
|
+
await makePlugin(root, "builtin-one")
|
|
139
|
+
const found = await discoverPlugins({ dirs: [root], builtinDirs: [root], logger: fakeLogger() })
|
|
140
|
+
expect(found[0]?.builtin).toBe(true)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it("被用户禁用的插件不出现在结果里", async () => {
|
|
144
|
+
await makePlugin(root, "nope")
|
|
145
|
+
const found = await discoverPlugins({ dirs: [root], disabled: ["nope"], logger: fakeLogger() })
|
|
146
|
+
expect(found).toHaveLength(0)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("相对路径目录被拒绝并警告(相对路径会随启动方式漂移)", async () => {
|
|
150
|
+
const logger = fakeLogger()
|
|
151
|
+
const found = await discoverPlugins({ dirs: ["./plugins"], logger })
|
|
152
|
+
expect(found).toHaveLength(0)
|
|
153
|
+
expect(logger.lines.some(l => l.includes("绝对路径"))).toBe(true)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("目录不存在时安静返回空数组", async () => {
|
|
157
|
+
const found = await discoverPlugins({ dirs: [join(root, "not-there")], logger: fakeLogger() })
|
|
158
|
+
expect(found).toEqual([])
|
|
159
|
+
})
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
describe("resolveLoadOrder", () => {
|
|
163
|
+
it("依赖先于依赖方加载", () => {
|
|
164
|
+
const { order, skipped } = resolveLoadOrder([
|
|
165
|
+
{ name: "game", dependencies: ["renderer"] },
|
|
166
|
+
{ name: "renderer" }
|
|
167
|
+
])
|
|
168
|
+
|
|
169
|
+
expect(order.map(u => u.name)).toEqual(["renderer", "game"])
|
|
170
|
+
expect(skipped).toEqual([])
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("多层依赖链按拓扑顺序展开", () => {
|
|
174
|
+
const { order } = resolveLoadOrder([
|
|
175
|
+
{ name: "c", dependencies: ["b"] },
|
|
176
|
+
{ name: "a" },
|
|
177
|
+
{ name: "b", dependencies: ["a"] }
|
|
178
|
+
])
|
|
179
|
+
expect(order.map(u => u.name)).toEqual(["a", "b", "c"])
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
it("同层内按 priority 再按名字排序(确定性)", () => {
|
|
183
|
+
const { order } = resolveLoadOrder([
|
|
184
|
+
{ name: "zebra", priority: 10 },
|
|
185
|
+
{ name: "alpha", priority: 50 },
|
|
186
|
+
{ name: "beta", priority: 50 },
|
|
187
|
+
{ name: "omega" }
|
|
188
|
+
])
|
|
189
|
+
expect(order.map(u => u.name)).toEqual(["zebra", "alpha", "beta", "omega"])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("同一集合的不同输入顺序得到同一结果", () => {
|
|
193
|
+
const units = [
|
|
194
|
+
{ name: "b", dependencies: ["a"] },
|
|
195
|
+
{ name: "a" },
|
|
196
|
+
{ name: "d", dependencies: ["a"] },
|
|
197
|
+
{ name: "c", dependencies: ["a"] }
|
|
198
|
+
]
|
|
199
|
+
const first = resolveLoadOrder(units).order.map(u => u.name)
|
|
200
|
+
const second = resolveLoadOrder([...units].reverse()).order.map(u => u.name)
|
|
201
|
+
expect(second).toEqual(first)
|
|
202
|
+
expect(first).toEqual(["a", "b", "c", "d"])
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("缺依赖的插件被跳过,其余照常加载", () => {
|
|
206
|
+
const { order, skipped } = resolveLoadOrder([{ name: "needs-genshin", dependencies: ["genshin"] }, { name: "solo" }])
|
|
207
|
+
|
|
208
|
+
expect(order.map(u => u.name)).toEqual(["solo"])
|
|
209
|
+
expect(skipped).toHaveLength(1)
|
|
210
|
+
expect(skipped[0]?.name).toBe("needs-genshin")
|
|
211
|
+
expect(skipped[0]?.reason).toContain("genshin")
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("依赖缺失会级联剔除依赖它的插件", () => {
|
|
215
|
+
const { order, skipped } = resolveLoadOrder([
|
|
216
|
+
{ name: "a", dependencies: ["missing"] },
|
|
217
|
+
{ name: "b", dependencies: ["a"] },
|
|
218
|
+
{ name: "c", dependencies: ["b"] },
|
|
219
|
+
{ name: "healthy" }
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
expect(order.map(u => u.name)).toEqual(["healthy"])
|
|
223
|
+
expect(skipped.map(s => s.name).sort()).toEqual(["a", "b", "c"])
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it("依赖成环时整环跳过,并在原因里列出环成员", () => {
|
|
227
|
+
const { order, skipped } = resolveLoadOrder([
|
|
228
|
+
{ name: "x", dependencies: ["y"] },
|
|
229
|
+
{ name: "y", dependencies: ["x"] },
|
|
230
|
+
{ name: "fine" }
|
|
231
|
+
])
|
|
232
|
+
|
|
233
|
+
expect(order.map(u => u.name)).toEqual(["fine"])
|
|
234
|
+
expect(skipped.map(s => s.name)).toEqual(["x", "y"])
|
|
235
|
+
expect(skipped[0]?.reason).toContain("成环")
|
|
236
|
+
expect(skipped[0]?.reason).toContain("y")
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("重名只留第一个", () => {
|
|
240
|
+
const { order, skipped } = resolveLoadOrder([
|
|
241
|
+
{ name: "dup", priority: 1 },
|
|
242
|
+
{ name: "dup", priority: 2 }
|
|
243
|
+
])
|
|
244
|
+
|
|
245
|
+
expect(order).toHaveLength(1)
|
|
246
|
+
expect(order[0]?.priority).toBe(1)
|
|
247
|
+
expect(skipped[0]?.reason).toContain("重复")
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
it("空输入不报错", () => {
|
|
251
|
+
expect(resolveLoadOrder([])).toEqual({ order: [], skipped: [] })
|
|
252
|
+
})
|
|
253
|
+
})
|