@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 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
+ }