@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.
@@ -0,0 +1,415 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ generateTempAstroTemplateFile,
4
+ generateTempRuntimeFile,
5
+ safeComponentName,
6
+ } from "./generateTempFile";
7
+
8
+ describe("safeComponentName", () => {
9
+ it("returns pascalCase name when it is a valid component identifier", () => {
10
+ expect(safeComponentName("hello-world", 0)).toBe("HelloWorld");
11
+ expect(safeComponentName("NavBar", 1)).toBe("NavBar");
12
+ expect(safeComponentName("foo_bar", 2)).toBe("FooBar");
13
+ });
14
+
15
+ it("falls back to HyacineComponent{index} for invalid/edge names", () => {
16
+ expect(safeComponentName("", 0)).toBe("HyacineComponent0");
17
+ expect(safeComponentName("123", 3)).toBe("HyacineComponent3");
18
+ expect(safeComponentName("-", 9)).toBe("HyacineComponent9");
19
+ // 中文/emoji 等在 pascalCase 后通常不满足 /^[A-Z][A-Za-z0-9]*$/
20
+ expect(safeComponentName("你好", 4)).toBe("HyacineComponent4");
21
+ expect(safeComponentName("😀", 5)).toBe("HyacineComponent5");
22
+ });
23
+ });
24
+
25
+ describe("generateTempAstroTemplateFile", () => {
26
+ it("generates imports and usage for custom-element entries", () => {
27
+ const out = generateTempAstroTemplateFile([
28
+ {
29
+ type: "custom-element",
30
+ injectPoint: "layout",
31
+ name: "code-block",
32
+ path: "./CodeBlock.svelte",
33
+ },
34
+ ]);
35
+
36
+ expect(out).toContain('import CodeBlock from "./CodeBlock.svelte";');
37
+ expect(out).toContain('<CodeBlock style="display: none;" />');
38
+ expect(out).toContain("//⚠️ This file is auto-generated");
39
+ });
40
+
41
+ it("generates ssr usage with safe client hydration attribute", () => {
42
+ const out = generateTempAstroTemplateFile([
43
+ {
44
+ type: "ssr",
45
+ platform: "astro",
46
+ injectPoint: "header",
47
+ name: "NavBar",
48
+ path: "./NavBar.svelte",
49
+ clientHydrationInstruction: "load",
50
+ },
51
+ ]);
52
+
53
+ expect(out).toContain('import NavBar from "./NavBar.svelte";');
54
+ expect(out).toContain("<NavBar client:load />");
55
+ });
56
+
57
+ it("supports multiple mixed entries (custom-element + ssr)", () => {
58
+ const out = generateTempAstroTemplateFile([
59
+ {
60
+ type: "custom-element",
61
+ injectPoint: "layout",
62
+ name: "code-block",
63
+ path: "./CodeBlock.svelte",
64
+ },
65
+ {
66
+ type: "ssr",
67
+ platform: "astro",
68
+ injectPoint: "header",
69
+ name: "NavBar",
70
+ path: "./NavBar.svelte",
71
+ clientHydrationInstruction: "idle",
72
+ },
73
+ {
74
+ type: "ssr",
75
+ platform: "astro",
76
+ injectPoint: "footer",
77
+ name: "Waves",
78
+ path: "./Waves.svelte",
79
+ },
80
+ ]);
81
+
82
+ // imports
83
+ expect(out).toContain('import CodeBlock from "./CodeBlock.svelte";');
84
+ expect(out).toContain('import NavBar from "./NavBar.svelte";');
85
+ expect(out).toContain('import Waves from "./Waves.svelte";');
86
+
87
+ // usage
88
+ expect(out).toContain('<CodeBlock style="display: none;" />');
89
+ expect(out).toContain("<NavBar client:idle />");
90
+ // 没有 hydration 指令时,不应有 client:* 属性
91
+ expect(out).toContain("<Waves />");
92
+ expect(out).not.toContain("<Waves client:");
93
+ });
94
+
95
+ it("dedupes repeated paths (defense against duplicate mount)", () => {
96
+ const out = generateTempAstroTemplateFile([
97
+ { type: "custom-element", injectPoint: "layout", name: "A", path: "./Same.svelte" },
98
+ // same path, different name/type should be ignored
99
+ {
100
+ type: "ssr",
101
+ platform: "astro",
102
+ injectPoint: "header",
103
+ name: "B",
104
+ path: "./Same.svelte",
105
+ clientHydrationInstruction: "load",
106
+ },
107
+ ]);
108
+
109
+ const importCount = (out.match(/\bimport\b/g) ?? []).length;
110
+ expect(importCount).toBe(1);
111
+ // usage 也只应出现一次
112
+ const sameTagCount = (out.match(/<\w+ style="display: none;" \/>/g) ?? []).length;
113
+ expect(sameTagCount).toBe(1);
114
+ // 不应出现第二个 entry 的 hydration
115
+ expect(out).not.toContain("client:load");
116
+ });
117
+
118
+ it("handles empty entries (edge case)", () => {
119
+ const out = generateTempAstroTemplateFile([]);
120
+ expect(out).toContain("---");
121
+ expect(out).toContain("<Fragment>");
122
+ // 不应包含任何 import
123
+ expect(out).not.toMatch(/\bimport\b/);
124
+ });
125
+
126
+ it("defuses malicious name injection by falling back to safeComponentName", () => {
127
+ const maliciousName = '"><script>alert(1)</script>';
128
+ const expectedComponentName = safeComponentName(maliciousName, 0);
129
+
130
+ const out = generateTempAstroTemplateFile([
131
+ {
132
+ type: "custom-element",
133
+ injectPoint: "layout", // 尝试把脚本注入到标签/标识符
134
+ name: maliciousName,
135
+ path: "./Evil.svelte",
136
+ },
137
+ ]);
138
+
139
+ expect(out).toContain(`import ${expectedComponentName} from "./Evil.svelte";`);
140
+ expect(out).toContain(`<${expectedComponentName} style="display: none;" />`);
141
+ expect(out).not.toContain("<script>");
142
+ expect(out).not.toContain("</script>");
143
+ });
144
+
145
+ it("defuses malicious path injection by escaping import string literal", () => {
146
+ const maliciousPath = "./ok';\nconsole.log('PWNED');\n//";
147
+ const out = generateTempAstroTemplateFile([
148
+ { type: "custom-element", injectPoint: "layout", name: "Safe", path: maliciousPath },
149
+ ]);
150
+
151
+ // JSON.stringify 会把真实换行转成 \n,避免生成文件出现新的语句行
152
+ expect(out).toContain(`import Safe from ${JSON.stringify(maliciousPath)};`);
153
+ // 不应出现真实的注入语句行(带真实换行的 console.log)
154
+ expect(out).not.toContain("\nconsole.log('PWNED');\n");
155
+ });
156
+
157
+ it("ignores malicious hydration instruction (runtime validation)", () => {
158
+ const out = generateTempAstroTemplateFile([
159
+ {
160
+ type: "ssr",
161
+ platform: "astro",
162
+ injectPoint: "header",
163
+ name: "NavBar",
164
+ path: "./NavBar.svelte",
165
+ // 绕过类型系统的注入:运行时必须忽略
166
+ clientHydrationInstruction: "load on:click=alert(1)" as any,
167
+ },
168
+ ]);
169
+
170
+ expect(out).toContain("<NavBar />");
171
+ expect(out).not.toContain("client:load on:click");
172
+ });
173
+ });
174
+
175
+ describe("generateTempRuntimeFile", () => {
176
+ const mockInjectPoints = {
177
+ footer: "#footer",
178
+ header: ".header",
179
+ sidebar: "[data-sidebar]",
180
+ };
181
+
182
+ it("generates imports and init calls for runtime-only entries", () => {
183
+ const out = generateTempRuntimeFile(
184
+ [{ type: "runtime-only", name: "mouse-firework", path: "./mouse-firework.ts" }],
185
+ mockInjectPoints,
186
+ );
187
+
188
+ // name 会变成 MouseFirework
189
+ expect(out).toContain('import { init as init_MouseFirework } from "./mouse-firework.ts";');
190
+ expect(out).toContain("init_MouseFirework({});");
191
+ });
192
+
193
+ it("falls back to HyacineComponent{index} for invalid runtime-only names", () => {
194
+ const out = generateTempRuntimeFile(
195
+ [{ type: "runtime-only", name: "123", path: "./runtime.ts" }],
196
+ mockInjectPoints,
197
+ );
198
+
199
+ expect(out).toContain('import { init as init_HyacineComponent0 } from "./runtime.ts";');
200
+ expect(out).toContain("init_HyacineComponent0({});");
201
+ });
202
+
203
+ it("supports multiple runtime-only entries (mixed valid + fallback)", () => {
204
+ const out = generateTempRuntimeFile(
205
+ [
206
+ { type: "runtime-only", name: "mouse-firework", path: "./mouse-firework.ts" },
207
+ { type: "runtime-only", name: "你好", path: "./cn.ts" },
208
+ ],
209
+ mockInjectPoints,
210
+ );
211
+
212
+ // 第 0 个:正常 pascalCase
213
+ expect(out).toContain('import { init as init_MouseFirework } from "./mouse-firework.ts";');
214
+ expect(out).toContain("init_MouseFirework({});");
215
+
216
+ // 第 1 个:fallback
217
+ expect(out).toContain('import { init as init_HyacineComponent1 } from "./cn.ts";');
218
+ expect(out).toContain("init_HyacineComponent1({});");
219
+ });
220
+
221
+ it("dedupes repeated paths (defense against duplicate init)", () => {
222
+ const out = generateTempRuntimeFile(
223
+ [
224
+ { type: "runtime-only", name: "A", path: "./same.ts" },
225
+ { type: "runtime-only", name: "B", path: "./same.ts" },
226
+ ],
227
+ mockInjectPoints,
228
+ );
229
+
230
+ // 去重后应该只有一个 init import
231
+ const initImportCount = (out.match(/import { init as init_/g) ?? []).length;
232
+ expect(initImportCount).toBe(1);
233
+
234
+ // init 调用也只应一次
235
+ const initCallCount = (out.match(/\binit_\w+\({.*?}\);/g) ?? []).length;
236
+ expect(initCallCount).toBe(1);
237
+ });
238
+
239
+ it("defuses malicious path injection in runtime file", () => {
240
+ const maliciousPath = "./ok';\nthrow new Error('PWNED');\n//";
241
+ const out = generateTempRuntimeFile(
242
+ [{ type: "runtime-only", name: "Safe", path: maliciousPath }],
243
+ mockInjectPoints,
244
+ );
245
+
246
+ expect(out).toContain(`from ${JSON.stringify(maliciousPath)};`);
247
+ expect(out).not.toContain("\nthrow new Error('PWNED');\n");
248
+ });
249
+
250
+ it("initializes global inject points map", () => {
251
+ const out = generateTempRuntimeFile([], mockInjectPoints);
252
+
253
+ // 验证全局变量被初始化
254
+ expect(out).toContain("globalThis.__HYACINE_INJECT_POINTS__ =");
255
+
256
+ // 验证 injectPoints 被正确序列化
257
+ expect(out).toContain('"footer": "#footer"');
258
+ expect(out).toContain('"header": ".header"');
259
+ expect(out).toContain('"sidebar": "[data-sidebar]"');
260
+ });
261
+
262
+ it("generates init calls with serialized options when provided", () => {
263
+ const out = generateTempRuntimeFile(
264
+ [
265
+ {
266
+ type: "runtime-only",
267
+ name: "mouse-firework",
268
+ path: "./mouse-firework.ts",
269
+ options: { theme: "dark", count: 42, enabled: true },
270
+ },
271
+ ],
272
+ mockInjectPoints,
273
+ );
274
+
275
+ // 验证 init 调用包含序列化的 options
276
+ expect(out).toContain('import { init as init_MouseFirework } from "./mouse-firework.ts";');
277
+ expect(out).toContain('init_MouseFirework({"theme":"dark","count":42,"enabled":true});');
278
+ });
279
+
280
+ it("generates init calls with empty object when no options provided (backward compatibility)", () => {
281
+ const out = generateTempRuntimeFile(
282
+ [{ type: "runtime-only", name: "legacy-plugin", path: "./legacy.ts" }],
283
+ mockInjectPoints,
284
+ );
285
+
286
+ // 验证向后兼容:无 options 时传入空对象
287
+ expect(out).toContain('import { init as init_LegacyPlugin } from "./legacy.ts";');
288
+ expect(out).toContain("init_LegacyPlugin({});");
289
+ });
290
+
291
+ it("handles complex options with nested objects and arrays", () => {
292
+ const out = generateTempRuntimeFile(
293
+ [
294
+ {
295
+ type: "runtime-only",
296
+ name: "complex",
297
+ path: "./complex.ts",
298
+ options: {
299
+ config: { nested: { deep: "value" } },
300
+ items: [1, 2, 3],
301
+ mixed: { arr: ["a", "b"], num: 123 },
302
+ },
303
+ },
304
+ ],
305
+ mockInjectPoints,
306
+ );
307
+
308
+ expect(out).toContain('import { init as init_Complex } from "./complex.ts";');
309
+ // 验证复杂对象被正确序列化(JSON.stringify 输出)
310
+ expect(out).toContain('"config":{"nested":{"deep":"value"}}');
311
+ expect(out).toContain('"items":[1,2,3]');
312
+ expect(out).toContain('"mixed":{"arr":["a","b"],"num":123}');
313
+ });
314
+
315
+ it("filters out invalid option keys for safety", () => {
316
+ const out = generateTempRuntimeFile(
317
+ [
318
+ {
319
+ type: "runtime-only",
320
+ name: "safe",
321
+ path: "./safe.ts",
322
+ options: {
323
+ validKey: "ok",
324
+ "invalid-key": "bad", // 包含连字符,不符合标识符规则
325
+ "123": "bad", // 以数字开头
326
+ },
327
+ },
328
+ ],
329
+ mockInjectPoints,
330
+ );
331
+
332
+ // 只有 validKey 应该被保留
333
+ expect(out).toContain('"validKey":"ok"');
334
+ expect(out).not.toContain("invalid-key");
335
+ expect(out).not.toContain('"123"');
336
+ });
337
+
338
+ it("filters out non-serializable values (functions, undefined)", () => {
339
+ const out = generateTempRuntimeFile(
340
+ [
341
+ {
342
+ type: "runtime-only",
343
+ name: "filtered",
344
+ path: "./filtered.ts",
345
+ options: {
346
+ goodString: "hello",
347
+ badFunction: (() => {}) as any, // 函数不可序列化
348
+ badUndefined: undefined, // undefined 会被 JSON.stringify 跳过
349
+ goodNull: null, // null 是可序列化的
350
+ },
351
+ },
352
+ ],
353
+ mockInjectPoints,
354
+ );
355
+
356
+ expect(out).toContain('"goodString":"hello"');
357
+ expect(out).toContain('"goodNull":null');
358
+ expect(out).not.toContain("badFunction");
359
+ expect(out).not.toContain("badUndefined");
360
+ });
361
+
362
+ it("defuses malicious options injection attempts", () => {
363
+ const maliciousValue = '"; console.log("PWNED"); //';
364
+ const out = generateTempRuntimeFile(
365
+ [
366
+ {
367
+ type: "runtime-only",
368
+ name: "evil",
369
+ path: "./evil.ts",
370
+ options: { message: maliciousValue },
371
+ },
372
+ ],
373
+ mockInjectPoints,
374
+ );
375
+
376
+ // JSON.stringify 会自动转义特殊字符,防止代码注入
377
+ expect(out).toContain('import { init as init_Evil } from "./evil.ts";');
378
+ // 验证恶意字符串被安全转义
379
+ expect(out).toContain(JSON.stringify({ message: maliciousValue }));
380
+ // 不应该出现裸露的注入代码
381
+ expect(out).not.toContain('"; console.log("PWNED"); //');
382
+ });
383
+
384
+ it("warns about duplicate paths with different options", () => {
385
+ // 这个测试验证去重逻辑:相同路径只保留第一个 entry
386
+ const consoleWarnSpy = console.warn;
387
+ const warnings: string[] = [];
388
+ console.warn = (msg: string) => warnings.push(msg);
389
+
390
+ generateTempRuntimeFile(
391
+ [
392
+ {
393
+ type: "runtime-only",
394
+ name: "First",
395
+ path: "./same.ts",
396
+ options: { version: 1 },
397
+ },
398
+ {
399
+ type: "runtime-only",
400
+ name: "Second",
401
+ path: "./same.ts",
402
+ options: { version: 2 },
403
+ },
404
+ ],
405
+ mockInjectPoints,
406
+ );
407
+
408
+ console.warn = consoleWarnSpy;
409
+
410
+ // 应该有警告提示存在不同 options 的重复路径
411
+ expect(warnings.some((w) => w.includes("Duplicate entry path") && w.includes("same.ts"))).toBe(
412
+ true,
413
+ );
414
+ });
415
+ });
@@ -0,0 +1,215 @@
1
+ import { pascalCase } from "es-toolkit";
2
+ import type { CustomElementEntry, RuntimeOnlyEntry, SSREntry } from "@hyacine/core";
3
+
4
+ export function safeComponentName(name: string, index: number) {
5
+ const pc = pascalCase(name);
6
+ if (/^[A-Z][A-Za-z0-9]*$/.test(pc)) {
7
+ return pc;
8
+ }
9
+ console.warn(
10
+ `[hyacine] Warning: Unsafe component name "${name}" converted to safe default "HyacineComponent${index}".`,
11
+ );
12
+ return `HyacineComponent${index}`;
13
+ }
14
+
15
+ function safeImportPath(path: string): string {
16
+ // 使用 JSON 字符串字面量来避免引号/换行等导致的代码注入
17
+ // 例:import X from "...";
18
+ return JSON.stringify(path);
19
+ }
20
+
21
+ function safeHydrationInstruction(
22
+ hydration: SSREntry["clientHydrationInstruction"],
23
+ ): SSREntry["clientHydrationInstruction"] | undefined {
24
+ // manifest 来自外部数据时,类型约束并不等于运行时安全;这里做白名单校验。
25
+ if (
26
+ hydration === "load" ||
27
+ hydration === "idle" ||
28
+ hydration === "visible" ||
29
+ hydration === "media"
30
+ ) {
31
+ return hydration;
32
+ }
33
+ console.warn(
34
+ `[hyacine] Warning: Invalid client hydration instruction "${hydration}" detected. Ignoring it for safety.`,
35
+ );
36
+ return undefined;
37
+ }
38
+
39
+ /**
40
+ * 将 props 对象序列化为 Astro 组件属性字符串
41
+ * 例如: { title: "Hello", count: 42 } => ' title="Hello" count={42}'
42
+ */
43
+ function serializeProps(props: Record<string, unknown>): string {
44
+ const entries = Object.entries(props);
45
+ if (entries.length === 0) return "";
46
+
47
+ return entries
48
+ .map(([key, value]) => {
49
+ // 校验 key 是否是安全的属性名
50
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
51
+ console.warn(`[hyacine] Warning: Invalid prop name "${key}" detected. Skipping this prop.`);
52
+ return "";
53
+ }
54
+
55
+ // 根据值的类型决定如何序列化
56
+ if (typeof value === "string") {
57
+ // 字符串值用双引号包裹,需要转义内部的引号和反斜杠
58
+ const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
59
+ return ` ${key}="${escaped}"`;
60
+ } else if (typeof value === "number" || typeof value === "boolean") {
61
+ // 数字和布尔值使用 JSX 表达式语法
62
+ return ` ${key}={${value}}`;
63
+ } else if (value === null || value === undefined) {
64
+ // null 和 undefined 跳过
65
+ return "";
66
+ } else {
67
+ // 对象、数组等复杂类型使用 JSON 序列化后作为 JSX 表达式
68
+ try {
69
+ const serialized = JSON.stringify(value);
70
+ return ` ${key}={${serialized}}`;
71
+ } catch (error) {
72
+ const errorMessage = error instanceof Error ? error.message : String(error);
73
+ console.warn(
74
+ `[hyacine] Warning: Failed to serialize prop "${key}": ${errorMessage}. Skipping this prop.`,
75
+ );
76
+ return "";
77
+ }
78
+ }
79
+ })
80
+ .filter((s) => s !== "")
81
+ .join("");
82
+ }
83
+
84
+ /**
85
+ * 将 options 对象序列化为 TypeScript/JavaScript 对象字面量字符串
86
+ * 用于 runtime-only 插件的 init 函数参数
87
+ * 例如: { theme: "dark", count: 42 } => '{"theme":"dark","count":42}'
88
+ */
89
+ function serializeOptionsForRuntime(options: Record<string, unknown>): string {
90
+ const filteredOptions: Record<string, unknown> = {};
91
+
92
+ for (const [key, value] of Object.entries(options)) {
93
+ // 校验 key 是否是安全的属性名
94
+ if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key)) {
95
+ console.warn(
96
+ `[hyacine] Warning: Invalid option key "${key}" detected. Skipping this option.`,
97
+ );
98
+ continue;
99
+ }
100
+
101
+ // 跳过 undefined 和函数(不可序列化)
102
+ if (value === undefined || typeof value === "function") {
103
+ console.warn(
104
+ `[hyacine] Warning: Option "${key}" has non-serializable value (${typeof value}). Skipping this option.`,
105
+ );
106
+ continue;
107
+ }
108
+
109
+ filteredOptions[key] = value;
110
+ }
111
+
112
+ try {
113
+ // 使用 JSON.stringify 序列化,这样可以安全地处理特殊字符和嵌套对象
114
+ return JSON.stringify(filteredOptions);
115
+ } catch (error) {
116
+ const errorMessage = error instanceof Error ? error.message : String(error);
117
+ console.warn(
118
+ `[hyacine] Warning: Failed to serialize options: ${errorMessage}. Using empty object.`,
119
+ );
120
+ return "{}";
121
+ }
122
+ }
123
+
124
+ export function generateTempAstroTemplateFile(entries: (SSREntry | CustomElementEntry)[]): string {
125
+ let importsArea = "";
126
+
127
+ let usageArea = "";
128
+
129
+ const seenPaths = new Set<string>();
130
+
131
+ entries.forEach((entry, index) => {
132
+ if (seenPaths.has(entry.path)) {
133
+ console.warn(
134
+ `[hyacine] Warning: Duplicate entry path "${entry.path}" detected. Skipping duplicate import.`,
135
+ );
136
+ return;
137
+ }
138
+ seenPaths.add(entry.path);
139
+
140
+ const componentName = safeComponentName(entry.name, index);
141
+ importsArea += `import ${componentName} from ${safeImportPath(entry.path)};\n`;
142
+
143
+ if (entry.type === "custom-element") {
144
+ usageArea += `<${componentName} style="display: none;" />\n`;
145
+ } else if (entry.type === "ssr") {
146
+ const hydration = safeHydrationInstruction(entry.clientHydrationInstruction);
147
+ const hydrationAttr = hydration ? ` client:${hydration}` : "";
148
+ const propsAttr = entry.props ? serializeProps(entry.props) : "";
149
+ usageArea += `<${componentName}${hydrationAttr}${propsAttr} />\n`;
150
+ }
151
+ });
152
+
153
+ return `
154
+ ---
155
+ //⚠️ This file is auto-generated by hyacine plugin system. Do not edit directly!
156
+ // @ts-nocheck
157
+ ${importsArea}
158
+ ---
159
+ <Fragment>
160
+ ${usageArea}
161
+ </Fragment>
162
+ `.trim();
163
+ }
164
+
165
+ export function generateTempRuntimeFile(
166
+ entries: RuntimeOnlyEntry[],
167
+ injectPoints: Record<string, string>,
168
+ ): string {
169
+ let importsArea = "";
170
+ let initArea = "";
171
+
172
+ const seenPaths = new Set<string>();
173
+
174
+ entries.forEach((entry, index) => {
175
+ if (seenPaths.has(entry.path)) {
176
+ if (entry.options && Object.keys(entry.options).length > 0) {
177
+ console.warn(
178
+ `[hyacine] Warning: Duplicate entry path "${entry.path}" with different options detected. Using the first entry's options.`,
179
+ );
180
+ }
181
+ return;
182
+ }
183
+ seenPaths.add(entry.path);
184
+
185
+ const componentName = safeComponentName(entry.name, index);
186
+ const initAlias = `init_${componentName}`;
187
+ importsArea += `import { init as ${initAlias} } from ${safeImportPath(entry.path)};\n`;
188
+
189
+ // 序列化 options,如果没有 options 则传空对象以保持向后兼容
190
+ const optionsArg = entry.options ? serializeOptionsForRuntime(entry.options) : "{}";
191
+ initArea += `${initAlias}(${optionsArg});\n`;
192
+ });
193
+
194
+ // 序列化 injectPoints 映射表
195
+ const serializedInjectPoints = JSON.stringify(injectPoints, null, 2);
196
+
197
+ return `
198
+ //⚠️ This file is auto-generated by hyacine plugin system. Do not edit directly!
199
+ // @ts-nocheck
200
+
201
+ /**
202
+ * Runtime initialization
203
+ *
204
+ * This file initializes the global inject points map and calls all runtime-only plugin init functions.
205
+ */
206
+
207
+ ${importsArea}
208
+
209
+ // 初始化全局注入点映射表
210
+ globalThis.__HYACINE_INJECT_POINTS__ = ${serializedInjectPoints};
211
+
212
+ // 调用所有 runtime-only 插件的 init 函数
213
+ ${initArea}
214
+ `.trim();
215
+ }