@hyacine/helper 0.0.1
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/package.json +33 -0
- package/src/dom-inject.test.ts +218 -0
- package/src/dom-inject.ts +223 -0
- package/src/generateTempFile.test.ts +415 -0
- package/src/generateTempFile.ts +215 -0
- package/src/getInjectPointSelector.test.ts +104 -0
- package/src/getInjectPointSelector.ts +50 -0
- package/src/index.ts +3 -0
- package/src/inject-point.test.ts +60 -0
- package/src/inject-point.ts +47 -0
- package/src/integration-utils.test.ts +298 -0
- package/src/integration-utils.ts +85 -0
- package/src/runtime.d.ts +158 -0
- package/src/runtime.test.ts +35 -0
- package/src/runtime.ts +20 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { getInjectPointSelector } from "./getInjectPointSelector";
|
|
3
|
+
|
|
4
|
+
describe("getInjectPointSelector", () => {
|
|
5
|
+
test("应该返回正确的选择器", () => {
|
|
6
|
+
const injectPoints = {
|
|
7
|
+
footer: "#footer",
|
|
8
|
+
sidebar: "#sidebar",
|
|
9
|
+
header: ".header-container",
|
|
10
|
+
"custom-point": "[data-custom]",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
expect(getInjectPointSelector("footer", injectPoints)).toBe("#footer");
|
|
14
|
+
expect(getInjectPointSelector("sidebar", injectPoints)).toBe("#sidebar");
|
|
15
|
+
expect(getInjectPointSelector("header", injectPoints)).toBe(".header-container");
|
|
16
|
+
expect(getInjectPointSelector("custom-point", injectPoints)).toBe("[data-custom]");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("当 injectPoint 不存在时应该抛出错误", () => {
|
|
20
|
+
const injectPoints = {
|
|
21
|
+
footer: "#footer",
|
|
22
|
+
sidebar: "#sidebar",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
expect(() => {
|
|
26
|
+
getInjectPointSelector("nonexistent", injectPoints);
|
|
27
|
+
}).toThrow(/未找到 injectPoint "nonexistent" 对应的选择器/);
|
|
28
|
+
|
|
29
|
+
expect(() => {
|
|
30
|
+
getInjectPointSelector("header", injectPoints);
|
|
31
|
+
}).toThrow(/未找到 injectPoint "header" 对应的选择器/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("当映射表无效时应该抛出错误", () => {
|
|
35
|
+
expect(() => {
|
|
36
|
+
// @ts-expect-error: 故意传入 null 以测试错误处理
|
|
37
|
+
getInjectPointSelector("footer", null);
|
|
38
|
+
}).toThrow(/injectPoints 映射表无效/);
|
|
39
|
+
|
|
40
|
+
expect(() => {
|
|
41
|
+
// @ts-expect-error: 故意传入 undefined 以测试错误处理
|
|
42
|
+
getInjectPointSelector("footer", undefined);
|
|
43
|
+
}).toThrow(/injectPoints 映射表无效/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("当选择器值为空字符串时应该抛出错误", () => {
|
|
47
|
+
const injectPoints = {
|
|
48
|
+
footer: "",
|
|
49
|
+
sidebar: " ",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(() => {
|
|
53
|
+
getInjectPointSelector("footer", injectPoints);
|
|
54
|
+
}).toThrow(/injectPoint "footer" 对应的选择器无效/);
|
|
55
|
+
|
|
56
|
+
expect(() => {
|
|
57
|
+
getInjectPointSelector("sidebar", injectPoints);
|
|
58
|
+
}).toThrow(/injectPoint "sidebar" 对应的选择器无效/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("错误消息应该包含可用的注入点列表", () => {
|
|
62
|
+
const injectPoints = {
|
|
63
|
+
footer: "#footer",
|
|
64
|
+
sidebar: "#sidebar",
|
|
65
|
+
header: ".header",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
getInjectPointSelector("nonexistent", injectPoints);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
expect(error).toBeInstanceOf(Error);
|
|
72
|
+
const message = (error as Error).message;
|
|
73
|
+
expect(message).toContain("footer");
|
|
74
|
+
expect(message).toContain("sidebar");
|
|
75
|
+
expect(message).toContain("header");
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("应该处理复杂的选择器", () => {
|
|
80
|
+
const injectPoints = {
|
|
81
|
+
complex: "div.container > .item:first-child",
|
|
82
|
+
attribute: '[data-inject="point"]',
|
|
83
|
+
pseudo: ".element::before",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
expect(getInjectPointSelector("complex", injectPoints)).toBe(
|
|
87
|
+
"div.container > .item:first-child",
|
|
88
|
+
);
|
|
89
|
+
expect(getInjectPointSelector("attribute", injectPoints)).toBe('[data-inject="point"]');
|
|
90
|
+
expect(getInjectPointSelector("pseudo", injectPoints)).toBe(".element::before");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("应该处理包含特殊字符的 injectPoint 键", () => {
|
|
94
|
+
const injectPoints = {
|
|
95
|
+
"post-footer": "#post-footer",
|
|
96
|
+
"footer-status": ".footer-status",
|
|
97
|
+
"segments-sticky": "[data-segments-sticky]",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
expect(getInjectPointSelector("post-footer", injectPoints)).toBe("#post-footer");
|
|
101
|
+
expect(getInjectPointSelector("footer-status", injectPoints)).toBe(".footer-status");
|
|
102
|
+
expect(getInjectPointSelector("segments-sticky", injectPoints)).toBe("[data-segments-sticky]");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 根据 injectPoint 获取对应的 querySelector 选择器
|
|
3
|
+
* 该函数会自动读取配置文件中的 injectPoints 作为映射表
|
|
4
|
+
*
|
|
5
|
+
* @param injectPoint - 注入点名称
|
|
6
|
+
* @param injectPointsMap - injectPoints 映射表(通常来自配置文件)
|
|
7
|
+
* @returns 对应的 querySelector 选择器
|
|
8
|
+
* @throws {Error} 如果映射表中不包含该 injectPoint
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { getInjectPointSelector } from './helper/getInjectPointSelector';
|
|
13
|
+
* import config from '../path/to/config';
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* const selector = getInjectPointSelector('footer', config.injectPoints);
|
|
17
|
+
* console.log(selector); // "#footer"
|
|
18
|
+
* } catch (error) {
|
|
19
|
+
* console.error(error.message);
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function getInjectPointSelector(
|
|
24
|
+
injectPoint: string,
|
|
25
|
+
injectPointsMap: Record<string, string>,
|
|
26
|
+
): string {
|
|
27
|
+
if (!injectPointsMap || typeof injectPointsMap !== "object") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[hyacine-plugin] injectPoints 映射表无效。收到的类型: ${typeof injectPointsMap}`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const selector = injectPointsMap[injectPoint];
|
|
34
|
+
|
|
35
|
+
if (selector === undefined || selector === null) {
|
|
36
|
+
const availablePoints = Object.keys(injectPointsMap).join(", ");
|
|
37
|
+
throw new Error(
|
|
38
|
+
`[hyacine-plugin] 未找到 injectPoint "${injectPoint}" 对应的选择器。` +
|
|
39
|
+
`\n可用的注入点有: ${availablePoints || "(无)"}`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (typeof selector !== "string" || selector.trim() === "") {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`[hyacine-plugin] injectPoint "${injectPoint}" 对应的选择器无效: ${JSON.stringify(selector)}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return selector;
|
|
50
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
function teardownInjectPoints() {
|
|
4
|
+
delete globalThis.__HYACINE_INJECT_POINTS__;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// 导入需要测试的函数
|
|
8
|
+
import { getInjectPointSelector } from "./inject-point";
|
|
9
|
+
|
|
10
|
+
describe("getInjectPointSelector", () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// 设置全局注入点映射表
|
|
13
|
+
globalThis.__HYACINE_INJECT_POINTS__ = {
|
|
14
|
+
footer: "#footer",
|
|
15
|
+
sidebar: "#sidebar",
|
|
16
|
+
header: ".header-container",
|
|
17
|
+
"custom-point": "[data-custom]",
|
|
18
|
+
};
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
teardownInjectPoints();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("应该返回正确的选择器", () => {
|
|
26
|
+
expect(getInjectPointSelector("footer")).toBe("#footer");
|
|
27
|
+
expect(getInjectPointSelector("sidebar")).toBe("#sidebar");
|
|
28
|
+
expect(getInjectPointSelector("header")).toBe(".header-container");
|
|
29
|
+
expect(getInjectPointSelector("custom-point")).toBe("[data-custom]");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("当 injectPoint 不存在时应该抛出错误", () => {
|
|
33
|
+
expect(() => {
|
|
34
|
+
getInjectPointSelector("nonexistent");
|
|
35
|
+
}).toThrow(/未找到 injectPoint "nonexistent" 对应的选择器/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("当映射表未初始化时应该抛出错误", () => {
|
|
39
|
+
delete globalThis.__HYACINE_INJECT_POINTS__;
|
|
40
|
+
|
|
41
|
+
expect(() => {
|
|
42
|
+
getInjectPointSelector("footer");
|
|
43
|
+
}).toThrow(/注入点映射表未初始化/);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("当选择器值为空字符串时应该抛出错误", () => {
|
|
47
|
+
globalThis.__HYACINE_INJECT_POINTS__ = {
|
|
48
|
+
footer: "",
|
|
49
|
+
sidebar: " ",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
expect(() => {
|
|
53
|
+
getInjectPointSelector("footer");
|
|
54
|
+
}).toThrow(/injectPoint "footer" 对应的选择器无效/);
|
|
55
|
+
|
|
56
|
+
expect(() => {
|
|
57
|
+
getInjectPointSelector("sidebar");
|
|
58
|
+
}).toThrow(/injectPoint "sidebar" 对应的选择器无效/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inject Point Selector API
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for retrieving CSS selectors for inject points.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// 扩展 globalThis 类型以包含注入点映射表
|
|
8
|
+
declare global {
|
|
9
|
+
// biome-ignore lint: Using var for global augmentation
|
|
10
|
+
var __HYACINE_INJECT_POINTS__: Record<string, string> | undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 获取注入点对应的 CSS 选择器
|
|
15
|
+
*
|
|
16
|
+
* Note: 此函数依赖于全局注入点映射表
|
|
17
|
+
* 该映射表由插件系统在运行时初始化
|
|
18
|
+
*
|
|
19
|
+
* @param injectPoint - 注入点名称
|
|
20
|
+
* @returns CSS 选择器字符串
|
|
21
|
+
* @throws {Error} 如果映射表中不包含该 injectPoint
|
|
22
|
+
*/
|
|
23
|
+
export function getInjectPointSelector(injectPoint: string): string {
|
|
24
|
+
const injectPointsMap = globalThis.__HYACINE_INJECT_POINTS__;
|
|
25
|
+
|
|
26
|
+
if (!injectPointsMap || typeof injectPointsMap !== "object") {
|
|
27
|
+
throw new Error(`[hyacine-plugin] 注入点映射表未初始化。请确保插件系统已正确初始化。`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const selector = injectPointsMap[injectPoint];
|
|
31
|
+
|
|
32
|
+
if (selector === undefined || selector === null) {
|
|
33
|
+
const availablePoints = Object.keys(injectPointsMap).join(", ");
|
|
34
|
+
throw new Error(
|
|
35
|
+
`[hyacine-plugin] 未找到 injectPoint "${injectPoint}" 对应的选择器。` +
|
|
36
|
+
`\n可用的注入点有: ${availablePoints || "(无)"}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof selector !== "string" || selector.trim() === "") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`[hyacine-plugin] injectPoint "${injectPoint}" 对应的选择器无效: ${JSON.stringify(selector)}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return selector;
|
|
47
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { HyacinePluginSystemConfig, PluginManifest } from "@hyacine/core";
|
|
6
|
+
import {
|
|
7
|
+
CONFIG_FILE_NAMES,
|
|
8
|
+
collectRuntimeOnlyEntries,
|
|
9
|
+
findConfigFilePath,
|
|
10
|
+
getPluginManifests,
|
|
11
|
+
groupEntriesByInjectPoint,
|
|
12
|
+
} from "./integration-utils";
|
|
13
|
+
|
|
14
|
+
describe("integration-utils", () => {
|
|
15
|
+
describe("CONFIG_FILE_NAMES", () => {
|
|
16
|
+
test("应该包含正确的配置文件名", () => {
|
|
17
|
+
expect(CONFIG_FILE_NAMES).toEqual(["hyacine.plugin.ts", "hyacine.plugin.mjs"]);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe("findConfigFilePath", () => {
|
|
22
|
+
test("应该找到 .ts 配置文件", () => {
|
|
23
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "hyacine-test-"));
|
|
24
|
+
const tsConfigPath = join(tmpDir, "hyacine.plugin.ts");
|
|
25
|
+
writeFileSync(tsConfigPath, "export default {};");
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = findConfigFilePath(tmpDir);
|
|
29
|
+
expect(result).toBe(tsConfigPath);
|
|
30
|
+
} finally {
|
|
31
|
+
rmSync(tmpDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("应该找到 .mjs 配置文件", () => {
|
|
36
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "hyacine-test-"));
|
|
37
|
+
const mjsConfigPath = join(tmpDir, "hyacine.plugin.mjs");
|
|
38
|
+
writeFileSync(mjsConfigPath, "export default {};");
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const result = findConfigFilePath(tmpDir);
|
|
42
|
+
expect(result).toBe(mjsConfigPath);
|
|
43
|
+
} finally {
|
|
44
|
+
rmSync(tmpDir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("应该优先选择 .ts 配置文件", () => {
|
|
49
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "hyacine-test-"));
|
|
50
|
+
const tsConfigPath = join(tmpDir, "hyacine.plugin.ts");
|
|
51
|
+
const mjsConfigPath = join(tmpDir, "hyacine.plugin.mjs");
|
|
52
|
+
writeFileSync(tsConfigPath, "export default {};");
|
|
53
|
+
writeFileSync(mjsConfigPath, "export default {};");
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result = findConfigFilePath(tmpDir);
|
|
57
|
+
expect(result).toBe(tsConfigPath);
|
|
58
|
+
} finally {
|
|
59
|
+
rmSync(tmpDir, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("找不到配置文件时应返回 null", () => {
|
|
64
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "hyacine-test-"));
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = findConfigFilePath(tmpDir);
|
|
68
|
+
expect(result).toBeNull();
|
|
69
|
+
} finally {
|
|
70
|
+
rmSync(tmpDir, { recursive: true });
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("getPluginManifests", () => {
|
|
76
|
+
test("应该返回配置中的插件列表", () => {
|
|
77
|
+
const mockConfig: HyacinePluginSystemConfig = {
|
|
78
|
+
injectPoints: {
|
|
79
|
+
header: "header",
|
|
80
|
+
footer: "footer",
|
|
81
|
+
},
|
|
82
|
+
plugins: [
|
|
83
|
+
{
|
|
84
|
+
name: "test-plugin-1",
|
|
85
|
+
version: "1.0.0",
|
|
86
|
+
minRenderCapability: "runtime-only" as const,
|
|
87
|
+
entry: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "test-plugin-2",
|
|
91
|
+
version: "1.0.0",
|
|
92
|
+
minRenderCapability: "runtime-only" as const,
|
|
93
|
+
entry: [],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const manifests = getPluginManifests(mockConfig);
|
|
99
|
+
|
|
100
|
+
expect(manifests).toHaveLength(2);
|
|
101
|
+
expect(manifests[0]!.name).toBe("test-plugin-1");
|
|
102
|
+
expect(manifests[1]!.name).toBe("test-plugin-2");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("collectRuntimeOnlyEntries", () => {
|
|
107
|
+
test("应该只收集 runtime-only 类型的 entry", () => {
|
|
108
|
+
const manifests: PluginManifest[] = [
|
|
109
|
+
{
|
|
110
|
+
name: "plugin-1",
|
|
111
|
+
version: "1.0.0",
|
|
112
|
+
minRenderCapability: "ssr" as const,
|
|
113
|
+
supportedPlatforms: ["astro"],
|
|
114
|
+
entry: [
|
|
115
|
+
{
|
|
116
|
+
type: "runtime-only",
|
|
117
|
+
name: "runtime-1",
|
|
118
|
+
path: "/path/to/runtime1.ts",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
type: "ssr",
|
|
122
|
+
name: "ssr-1",
|
|
123
|
+
path: "/path/to/ssr1.ts",
|
|
124
|
+
injectPoint: "header",
|
|
125
|
+
platform: "astro",
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "plugin-2",
|
|
131
|
+
version: "1.0.0",
|
|
132
|
+
minRenderCapability: "ssr" as const,
|
|
133
|
+
supportedPlatforms: ["astro"],
|
|
134
|
+
entry: [
|
|
135
|
+
{
|
|
136
|
+
type: "runtime-only",
|
|
137
|
+
name: "runtime-2",
|
|
138
|
+
path: "/path/to/runtime2.ts",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
type: "custom-element",
|
|
142
|
+
name: "ce-1",
|
|
143
|
+
path: "/path/to/ce1.ts",
|
|
144
|
+
injectPoint: "layout",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
const result = collectRuntimeOnlyEntries(manifests);
|
|
151
|
+
|
|
152
|
+
expect(result).toHaveLength(2);
|
|
153
|
+
expect(result[0]!.type).toBe("runtime-only");
|
|
154
|
+
expect(result[0]!.name).toBe("runtime-1");
|
|
155
|
+
expect(result[1]!.type).toBe("runtime-only");
|
|
156
|
+
expect(result[1]!.name).toBe("runtime-2");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("所有 entry 都不是 runtime-only 时应返回空数组", () => {
|
|
160
|
+
const manifests: PluginManifest[] = [
|
|
161
|
+
{
|
|
162
|
+
name: "plugin-1",
|
|
163
|
+
version: "1.0.0",
|
|
164
|
+
minRenderCapability: "ssr" as const,
|
|
165
|
+
supportedPlatforms: ["astro"],
|
|
166
|
+
entry: [
|
|
167
|
+
{
|
|
168
|
+
type: "ssr",
|
|
169
|
+
name: "ssr-1",
|
|
170
|
+
path: "/path/to/ssr1.ts",
|
|
171
|
+
injectPoint: "header",
|
|
172
|
+
platform: "astro",
|
|
173
|
+
},
|
|
174
|
+
],
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
const result = collectRuntimeOnlyEntries(manifests);
|
|
179
|
+
|
|
180
|
+
expect(result).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("空 manifests 应返回空数组", () => {
|
|
184
|
+
const result = collectRuntimeOnlyEntries([]);
|
|
185
|
+
expect(result).toHaveLength(0);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
describe("groupEntriesByInjectPoint", () => {
|
|
190
|
+
test("应该按 injectPoint 对 SSR 和 CustomElement entry 进行分组", () => {
|
|
191
|
+
const manifests: PluginManifest[] = [
|
|
192
|
+
{
|
|
193
|
+
name: "plugin-1",
|
|
194
|
+
version: "1.0.0",
|
|
195
|
+
minRenderCapability: "ssr" as const,
|
|
196
|
+
supportedPlatforms: ["astro"],
|
|
197
|
+
entry: [
|
|
198
|
+
{
|
|
199
|
+
type: "ssr",
|
|
200
|
+
name: "ssr-1",
|
|
201
|
+
path: "/path/to/ssr1.ts",
|
|
202
|
+
injectPoint: "header",
|
|
203
|
+
platform: "astro",
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
type: "ssr",
|
|
207
|
+
name: "ssr-2",
|
|
208
|
+
path: "/path/to/ssr2.ts",
|
|
209
|
+
injectPoint: "footer",
|
|
210
|
+
platform: "astro",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: "plugin-2",
|
|
216
|
+
version: "1.0.0",
|
|
217
|
+
minRenderCapability: "custom-element" as const,
|
|
218
|
+
supportedPlatforms: ["astro"],
|
|
219
|
+
entry: [
|
|
220
|
+
{
|
|
221
|
+
type: "custom-element",
|
|
222
|
+
name: "ce-1",
|
|
223
|
+
path: "/path/to/ce1.ts",
|
|
224
|
+
injectPoint: "layout",
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
];
|
|
229
|
+
|
|
230
|
+
const result = groupEntriesByInjectPoint(manifests);
|
|
231
|
+
|
|
232
|
+
expect(result.size).toBe(3);
|
|
233
|
+
expect(result.get("header")).toHaveLength(1);
|
|
234
|
+
expect(result.get("footer")).toHaveLength(1);
|
|
235
|
+
expect(result.get("layout")).toHaveLength(1);
|
|
236
|
+
expect(result.get("header")?.[0]?.name).toBe("ssr-1");
|
|
237
|
+
expect(result.get("footer")?.[0]?.name).toBe("ssr-2");
|
|
238
|
+
expect(result.get("layout")?.[0]?.name).toBe("ce-1");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("应该排除 runtime-only entry", () => {
|
|
242
|
+
const manifests: PluginManifest[] = [
|
|
243
|
+
{
|
|
244
|
+
name: "plugin-1",
|
|
245
|
+
version: "1.0.0",
|
|
246
|
+
minRenderCapability: "ssr" as const,
|
|
247
|
+
supportedPlatforms: ["astro"],
|
|
248
|
+
entry: [
|
|
249
|
+
{
|
|
250
|
+
type: "runtime-only",
|
|
251
|
+
name: "runtime-1",
|
|
252
|
+
path: "/path/to/runtime1.ts",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
type: "ssr",
|
|
256
|
+
name: "ssr-1",
|
|
257
|
+
path: "/path/to/ssr1.ts",
|
|
258
|
+
injectPoint: "header",
|
|
259
|
+
platform: "astro",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
263
|
+
];
|
|
264
|
+
|
|
265
|
+
const result = groupEntriesByInjectPoint(manifests);
|
|
266
|
+
|
|
267
|
+
expect(result.size).toBe(1);
|
|
268
|
+
expect(result.get("header")).toHaveLength(1);
|
|
269
|
+
expect(result.get("header")?.[0]?.name).toBe("ssr-1");
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("所有 entry 都是 runtime-only 时应返回空 Map", () => {
|
|
273
|
+
const manifests: PluginManifest[] = [
|
|
274
|
+
{
|
|
275
|
+
name: "plugin-1",
|
|
276
|
+
version: "1.0.0",
|
|
277
|
+
minRenderCapability: "runtime-only" as const,
|
|
278
|
+
entry: [
|
|
279
|
+
{
|
|
280
|
+
type: "runtime-only",
|
|
281
|
+
name: "runtime-1",
|
|
282
|
+
path: "/path/to/runtime1.ts",
|
|
283
|
+
},
|
|
284
|
+
],
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
|
|
288
|
+
const result = groupEntriesByInjectPoint(manifests);
|
|
289
|
+
|
|
290
|
+
expect(result.size).toBe(0);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("空 manifests 应返回空 Map", () => {
|
|
294
|
+
const result = groupEntriesByInjectPoint([]);
|
|
295
|
+
expect(result.size).toBe(0);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import type {
|
|
4
|
+
CustomElementEntry,
|
|
5
|
+
HyacinePluginSystemConfig,
|
|
6
|
+
PluginManifest,
|
|
7
|
+
RuntimeOnlyEntry,
|
|
8
|
+
SSREntry,
|
|
9
|
+
} from "@hyacine/core";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 配置文件名常量
|
|
13
|
+
*/
|
|
14
|
+
export const CONFIG_FILE_NAMES = ["hyacine.plugin.ts", "hyacine.plugin.mjs"] as const;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 查找插件配置文件路径
|
|
18
|
+
* @param rootPath 项目根目录路径
|
|
19
|
+
* @returns 配置文件的绝对路径,如果未找到则返回 null
|
|
20
|
+
*/
|
|
21
|
+
export function findConfigFilePath(rootPath: string): string | null {
|
|
22
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
23
|
+
const configPath = join(rootPath, fileName);
|
|
24
|
+
if (existsSync(configPath)) {
|
|
25
|
+
return configPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 获取插件 manifest 列表
|
|
33
|
+
* @param config 插件系统配置
|
|
34
|
+
* @returns manifest 列表
|
|
35
|
+
*/
|
|
36
|
+
export function getPluginManifests(config: HyacinePluginSystemConfig): PluginManifest[] {
|
|
37
|
+
return config.plugins;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 收集所有 runtime-only 类型的 entry(不包含警告逻辑)
|
|
42
|
+
* @param manifests manifest 列表
|
|
43
|
+
* @returns runtime-only entry 数组
|
|
44
|
+
*/
|
|
45
|
+
export function collectRuntimeOnlyEntries(manifests: PluginManifest[]): RuntimeOnlyEntry[] {
|
|
46
|
+
const entries: RuntimeOnlyEntry[] = [];
|
|
47
|
+
|
|
48
|
+
for (const manifest of manifests) {
|
|
49
|
+
for (const entry of manifest.entry) {
|
|
50
|
+
if (entry.type === "runtime-only") {
|
|
51
|
+
entries.push(entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 按照 injectPoint 对 entry 进行分组(排除 runtime-only entry)
|
|
61
|
+
* @param manifests manifest 列表
|
|
62
|
+
* @returns 按 injectPoint 分组的 entry map
|
|
63
|
+
*/
|
|
64
|
+
export function groupEntriesByInjectPoint(
|
|
65
|
+
manifests: PluginManifest[],
|
|
66
|
+
): Map<string, Array<SSREntry | CustomElementEntry>> {
|
|
67
|
+
const groups = new Map<string, Array<SSREntry | CustomElementEntry>>();
|
|
68
|
+
|
|
69
|
+
for (const manifest of manifests) {
|
|
70
|
+
for (const entry of manifest.entry) {
|
|
71
|
+
if (entry.type === "runtime-only") {
|
|
72
|
+
// runtime-only 单独处理,不在这里分组
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const injectPoint = entry.injectPoint;
|
|
77
|
+
if (!groups.has(injectPoint)) {
|
|
78
|
+
groups.set(injectPoint, []);
|
|
79
|
+
}
|
|
80
|
+
groups.get(injectPoint)!.push(entry);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return groups;
|
|
85
|
+
}
|