@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
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hyacine/helper",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist",
|
|
6
|
+
"src"
|
|
7
|
+
],
|
|
8
|
+
"type": "module",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"default": "./src/index.ts"
|
|
13
|
+
},
|
|
14
|
+
"./runtime": {
|
|
15
|
+
"types": "./src/runtime.d.ts",
|
|
16
|
+
"default": "./src/runtime.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "bun test",
|
|
22
|
+
"lint": "bunx --bun oxlint --type-aware --type-check . --fix",
|
|
23
|
+
"format": "bunx oxfmt ."
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@hyacine/core": "workspace:*",
|
|
27
|
+
"es-toolkit": "^1.44.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/bun": "latest",
|
|
31
|
+
"typescript": "^5"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 模拟浏览器环境
|
|
5
|
+
*/
|
|
6
|
+
function setupDOM() {
|
|
7
|
+
// 模拟 document 和基本的 DOM 方法
|
|
8
|
+
const elements = new Map<string, HTMLElement>();
|
|
9
|
+
|
|
10
|
+
const mockDocument = {
|
|
11
|
+
createElement: (tagName: string) => {
|
|
12
|
+
const element = {
|
|
13
|
+
tagName: tagName.toUpperCase(),
|
|
14
|
+
textContent: "",
|
|
15
|
+
src: "",
|
|
16
|
+
href: "",
|
|
17
|
+
type: "",
|
|
18
|
+
async: false,
|
|
19
|
+
defer: false,
|
|
20
|
+
crossOrigin: "",
|
|
21
|
+
integrity: "",
|
|
22
|
+
rel: "",
|
|
23
|
+
media: "",
|
|
24
|
+
attributes: {} as Record<string, string>,
|
|
25
|
+
setAttribute: (name: string, value: string) => {
|
|
26
|
+
element.attributes[name] = value;
|
|
27
|
+
},
|
|
28
|
+
getAttribute: (name: string) => element.attributes[name],
|
|
29
|
+
};
|
|
30
|
+
return element;
|
|
31
|
+
},
|
|
32
|
+
head: {
|
|
33
|
+
appendChild: (element: unknown) => {
|
|
34
|
+
elements.set("head-child", element as HTMLElement);
|
|
35
|
+
return element;
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
body: {
|
|
39
|
+
appendChild: (element: unknown) => {
|
|
40
|
+
elements.set("body-child", element as HTMLElement);
|
|
41
|
+
return element;
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// 将模拟的 document 设置到 global 作用域
|
|
47
|
+
// @ts-expect-error: 模拟 DOM 环境
|
|
48
|
+
global.document = mockDocument;
|
|
49
|
+
|
|
50
|
+
return { mockDocument, elements };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function teardownDOM() {
|
|
54
|
+
// @ts-expect-error: 清理模拟的 DOM
|
|
55
|
+
delete global.document;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 导入需要测试的函数(必须在 DOM 模拟之后)
|
|
59
|
+
import { injectLinkCSS, injectScript } from "./dom-inject";
|
|
60
|
+
|
|
61
|
+
describe("injectScript", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
setupDOM();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
afterEach(() => {
|
|
67
|
+
teardownDOM();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("应该注入外部脚本到 head", () => {
|
|
71
|
+
const script = injectScript({
|
|
72
|
+
src: "https://example.com/script.js",
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(script.tagName).toBe("SCRIPT");
|
|
76
|
+
expect(script.src).toBe("https://example.com/script.js");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("应该注入内联脚本到 head", () => {
|
|
80
|
+
const script = injectScript({
|
|
81
|
+
content: 'console.log("test");',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(script.tagName).toBe("SCRIPT");
|
|
85
|
+
expect(script.textContent).toBe('console.log("test");');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("应该注入脚本到 body", () => {
|
|
89
|
+
const script = injectScript(
|
|
90
|
+
{
|
|
91
|
+
src: "https://example.com/script.js",
|
|
92
|
+
},
|
|
93
|
+
"body",
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
expect(script.tagName).toBe("SCRIPT");
|
|
97
|
+
expect(script.src).toBe("https://example.com/script.js");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("应该设置 async 属性", () => {
|
|
101
|
+
const script = injectScript({
|
|
102
|
+
src: "https://example.com/script.js",
|
|
103
|
+
async: true,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(script.async).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("应该设置 defer 属性", () => {
|
|
110
|
+
const script = injectScript({
|
|
111
|
+
src: "https://example.com/script.js",
|
|
112
|
+
defer: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(script.defer).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("应该设置 type 属性", () => {
|
|
119
|
+
const script = injectScript({
|
|
120
|
+
src: "https://example.com/script.js",
|
|
121
|
+
type: "module",
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
expect(script.type).toBe("module");
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("应该设置 crossOrigin 和 integrity", () => {
|
|
128
|
+
const script = injectScript({
|
|
129
|
+
src: "https://example.com/script.js",
|
|
130
|
+
crossOrigin: "anonymous",
|
|
131
|
+
integrity: "sha384-xxx",
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(script.crossOrigin).toBe("anonymous");
|
|
135
|
+
expect(script.integrity).toBe("sha384-xxx");
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("应该设置自定义属性", () => {
|
|
139
|
+
const script = injectScript({
|
|
140
|
+
src: "https://example.com/script.js",
|
|
141
|
+
attributes: {
|
|
142
|
+
"data-custom": "value",
|
|
143
|
+
id: "my-script",
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(script.getAttribute("data-custom")).toBe("value");
|
|
148
|
+
expect(script.getAttribute("id")).toBe("my-script");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("当既没有 src 也没有 content 时应该抛出错误", () => {
|
|
152
|
+
expect(() => {
|
|
153
|
+
injectScript({});
|
|
154
|
+
}).toThrow(/必须提供 src 或 content 之一/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("当同时提供 src 和 content 时应该抛出错误", () => {
|
|
158
|
+
expect(() => {
|
|
159
|
+
injectScript({
|
|
160
|
+
src: "https://example.com/script.js",
|
|
161
|
+
content: 'console.log("test");',
|
|
162
|
+
});
|
|
163
|
+
}).toThrow(/src 和 content 不能同时提供/);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe("injectLinkCSS", () => {
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
setupDOM();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
afterEach(() => {
|
|
173
|
+
teardownDOM();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("应该注入 CSS 链接到 head", () => {
|
|
177
|
+
const link = injectLinkCSS({
|
|
178
|
+
href: "https://example.com/style.css",
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
expect(link.tagName).toBe("LINK");
|
|
182
|
+
expect(link.rel).toBe("stylesheet");
|
|
183
|
+
expect(link.href).toBe("https://example.com/style.css");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("应该设置 media 属性", () => {
|
|
187
|
+
const link = injectLinkCSS({
|
|
188
|
+
href: "/mobile.css",
|
|
189
|
+
media: "(max-width: 768px)",
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(link.media).toBe("(max-width: 768px)");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("应该设置 crossOrigin 和 integrity", () => {
|
|
196
|
+
const link = injectLinkCSS({
|
|
197
|
+
href: "https://cdn.example.com/library.css",
|
|
198
|
+
crossOrigin: "anonymous",
|
|
199
|
+
integrity: "sha384-xxx",
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(link.crossOrigin).toBe("anonymous");
|
|
203
|
+
expect(link.integrity).toBe("sha384-xxx");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("应该设置自定义属性", () => {
|
|
207
|
+
const link = injectLinkCSS({
|
|
208
|
+
href: "/style.css",
|
|
209
|
+
attributes: {
|
|
210
|
+
"data-theme": "dark",
|
|
211
|
+
id: "theme-css",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(link.getAttribute("data-theme")).toBe("dark");
|
|
216
|
+
expect(link.getAttribute("id")).toBe("theme-css");
|
|
217
|
+
});
|
|
218
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Injection API
|
|
3
|
+
*
|
|
4
|
+
* This module provides utilities for injecting scripts and CSS into the DOM.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Script 标签配置选项
|
|
9
|
+
*/
|
|
10
|
+
export interface ScriptOptions {
|
|
11
|
+
/**
|
|
12
|
+
* script 标签的 src 属性(外部脚本)
|
|
13
|
+
*/
|
|
14
|
+
src?: string;
|
|
15
|
+
/**
|
|
16
|
+
* script 标签的内联内容(内联脚本)
|
|
17
|
+
*/
|
|
18
|
+
content?: string;
|
|
19
|
+
/**
|
|
20
|
+
* script 标签的类型,默认为 "text/javascript"
|
|
21
|
+
*/
|
|
22
|
+
type?: string;
|
|
23
|
+
/**
|
|
24
|
+
* 是否异步加载脚本
|
|
25
|
+
*/
|
|
26
|
+
async?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* 是否延迟加载脚本
|
|
29
|
+
*/
|
|
30
|
+
defer?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* 跨域设置
|
|
33
|
+
*/
|
|
34
|
+
crossOrigin?: "anonymous" | "use-credentials";
|
|
35
|
+
/**
|
|
36
|
+
* 脚本的完整性校验值(Subresource Integrity)
|
|
37
|
+
*/
|
|
38
|
+
integrity?: string;
|
|
39
|
+
/**
|
|
40
|
+
* 其他自定义属性
|
|
41
|
+
*/
|
|
42
|
+
attributes?: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Link CSS 标签配置选项
|
|
47
|
+
*/
|
|
48
|
+
export interface LinkCSSOptions {
|
|
49
|
+
/**
|
|
50
|
+
* link 标签的 href 属性(CSS 文件路径)
|
|
51
|
+
*/
|
|
52
|
+
href: string;
|
|
53
|
+
/**
|
|
54
|
+
* 跨域设置
|
|
55
|
+
*/
|
|
56
|
+
crossOrigin?: "anonymous" | "use-credentials";
|
|
57
|
+
/**
|
|
58
|
+
* 资源的完整性校验值(Subresource Integrity)
|
|
59
|
+
*/
|
|
60
|
+
integrity?: string;
|
|
61
|
+
/**
|
|
62
|
+
* 媒体查询条件
|
|
63
|
+
*/
|
|
64
|
+
media?: string;
|
|
65
|
+
/**
|
|
66
|
+
* 其他自定义属性
|
|
67
|
+
*/
|
|
68
|
+
attributes?: Record<string, string>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 向 head 或 body 注入 script 标签
|
|
73
|
+
*
|
|
74
|
+
* @param options - Script 标签配置选项
|
|
75
|
+
* @param target - 注入目标,默认为 'head'
|
|
76
|
+
* @returns 创建的 script 元素
|
|
77
|
+
* @throws {Error} 如果既没有提供 src 也没有提供 content
|
|
78
|
+
*
|
|
79
|
+
* @example
|
|
80
|
+
* ```typescript
|
|
81
|
+
* // 注入外部脚本到 head
|
|
82
|
+
* injectScript({
|
|
83
|
+
* src: 'https://example.com/script.js',
|
|
84
|
+
* async: true
|
|
85
|
+
* });
|
|
86
|
+
*
|
|
87
|
+
* // 注入内联脚本到 body
|
|
88
|
+
* injectScript({
|
|
89
|
+
* content: 'console.log("Hello from inline script");'
|
|
90
|
+
* }, 'body');
|
|
91
|
+
*
|
|
92
|
+
* // 注入带有完整性校验的外部脚本
|
|
93
|
+
* injectScript({
|
|
94
|
+
* src: 'https://cdn.example.com/library.js',
|
|
95
|
+
* integrity: 'sha384-xxx',
|
|
96
|
+
* crossOrigin: 'anonymous'
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export function injectScript(
|
|
101
|
+
options: ScriptOptions,
|
|
102
|
+
target: "head" | "body" = "head",
|
|
103
|
+
): HTMLScriptElement {
|
|
104
|
+
if (!options.src && !options.content) {
|
|
105
|
+
throw new Error("[hyacine-plugin] injectScript: 必须提供 src 或 content 之一");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (options.src && options.content) {
|
|
109
|
+
throw new Error("[hyacine-plugin] injectScript: src 和 content 不能同时提供");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const script = document.createElement("script");
|
|
113
|
+
|
|
114
|
+
// 设置基本属性
|
|
115
|
+
if (options.src) {
|
|
116
|
+
script.src = options.src;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (options.content) {
|
|
120
|
+
script.textContent = options.content;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.type) {
|
|
124
|
+
script.type = options.type;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (options.async) {
|
|
128
|
+
script.async = true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (options.defer) {
|
|
132
|
+
script.defer = true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (options.crossOrigin) {
|
|
136
|
+
script.crossOrigin = options.crossOrigin;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (options.integrity) {
|
|
140
|
+
script.integrity = options.integrity;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 设置自定义属性
|
|
144
|
+
if (options.attributes) {
|
|
145
|
+
for (const [key, value] of Object.entries(options.attributes)) {
|
|
146
|
+
script.setAttribute(key, value);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 注入到目标位置
|
|
151
|
+
const targetElement = document[target];
|
|
152
|
+
if (!targetElement) {
|
|
153
|
+
throw new Error(`[hyacine-plugin] injectScript: 找不到目标元素 <${target}>`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
targetElement.appendChild(script);
|
|
157
|
+
|
|
158
|
+
return script;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* 向 head 注入 link CSS 标签
|
|
163
|
+
*
|
|
164
|
+
* @param options - Link CSS 标签配置选项
|
|
165
|
+
* @returns 创建的 link 元素
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* ```typescript
|
|
169
|
+
* // 注入外部 CSS
|
|
170
|
+
* injectLinkCSS({
|
|
171
|
+
* href: 'https://example.com/style.css'
|
|
172
|
+
* });
|
|
173
|
+
*
|
|
174
|
+
* // 注入带有媒体查询的 CSS
|
|
175
|
+
* injectLinkCSS({
|
|
176
|
+
* href: '/mobile.css',
|
|
177
|
+
* media: '(max-width: 768px)'
|
|
178
|
+
* });
|
|
179
|
+
*
|
|
180
|
+
* // 注入带有完整性校验的 CSS
|
|
181
|
+
* injectLinkCSS({
|
|
182
|
+
* href: 'https://cdn.example.com/library.css',
|
|
183
|
+
* integrity: 'sha384-xxx',
|
|
184
|
+
* crossOrigin: 'anonymous'
|
|
185
|
+
* });
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
export function injectLinkCSS(options: LinkCSSOptions): HTMLLinkElement {
|
|
189
|
+
const link = document.createElement("link");
|
|
190
|
+
|
|
191
|
+
// 设置固定属性
|
|
192
|
+
link.rel = "stylesheet";
|
|
193
|
+
link.href = options.href;
|
|
194
|
+
|
|
195
|
+
if (options.crossOrigin) {
|
|
196
|
+
link.crossOrigin = options.crossOrigin;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (options.integrity) {
|
|
200
|
+
link.integrity = options.integrity;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (options.media) {
|
|
204
|
+
link.media = options.media;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 设置自定义属性
|
|
208
|
+
if (options.attributes) {
|
|
209
|
+
for (const [key, value] of Object.entries(options.attributes)) {
|
|
210
|
+
link.setAttribute(key, value);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// 注入到 head
|
|
215
|
+
const head = document.head;
|
|
216
|
+
if (!head) {
|
|
217
|
+
throw new Error("[hyacine-plugin] injectLinkCSS: 找不到 <head> 元素");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
head.appendChild(link);
|
|
221
|
+
|
|
222
|
+
return link;
|
|
223
|
+
}
|