@hyacine/astro 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,38 @@
1
+ {
2
+ "name": "@hyacine/astro",
3
+ "version": "0.0.1",
4
+ "files": [
5
+ "dist",
6
+ "src"
7
+ ],
8
+ "type": "module",
9
+ "typesVersions": {
10
+ "*": {
11
+ "*": [
12
+ "./src/virtual-modules.d.ts"
13
+ ]
14
+ }
15
+ },
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ }
21
+ },
22
+ "scripts": {
23
+ "build": "tsc",
24
+ "lint": "bunx --bun oxlint --type-aware --type-check . --fix",
25
+ "format": "bunx oxfmt ."
26
+ },
27
+ "dependencies": {
28
+ "@hyacine/core": "workspace:*",
29
+ "@hyacine/helper": "workspace:*"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^5",
33
+ "vite": "^7.3.1"
34
+ },
35
+ "peerDependencies": {
36
+ "astro": "^5.16.11"
37
+ }
38
+ }
@@ -0,0 +1,278 @@
1
+ import type { AstroIntegration } from "astro";
2
+ import { existsSync } from "node:fs";
3
+ import type { Plugin as VitePlugin } from "vite";
4
+ import { join } from "node:path";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import type { HyacinePluginSystemConfig } from "@hyacine/core";
7
+ import type { PluginManifest } from "@hyacine/core";
8
+ import {
9
+ CONFIG_FILE_NAMES,
10
+ collectRuntimeOnlyEntries,
11
+ findConfigFilePath,
12
+ generateTempAstroTemplateFile,
13
+ generateTempRuntimeFile,
14
+ getPluginManifests,
15
+ groupEntriesByInjectPoint,
16
+ } from "@hyacine/helper";
17
+ import type { ViteDevServer } from "vite";
18
+
19
+ // 虚拟模块 ID 常量
20
+ const RUNTIME_VIRTUAL_ID = "virtual:hyacine/runtime";
21
+ const RUNTIME_RESOLVED_ID = "\0virtual:hyacine/runtime";
22
+
23
+ // Astro 组件虚拟模块 ID 前缀
24
+ const ASTRO_VIRTUAL_ID_PREFIX = "virtual:hyacine/inject/";
25
+ const ASTRO_RESOLVED_ID_PREFIX = "\0virtual:hyacine/inject/";
26
+
27
+ function toViteFsPath(filePath: string): string {
28
+ // Vite 的 /@fs/ 需要 POSIX 风格路径分隔符
29
+ return `/@fs/${filePath.replaceAll("\\", "/")}`;
30
+ }
31
+
32
+ /**
33
+ * 读取插件配置文件
34
+ * 支持 hyacine.plugin.ts 和 hyacine.plugin.mjs
35
+ * @param root 项目根目录 URL
36
+ * @param bypassCache 是否绕过模块缓存(开发模式 HMR 时使用)
37
+ */
38
+ async function loadPluginConfig(
39
+ root: URL,
40
+ bypassCache = false,
41
+ viteServer?: Pick<ViteDevServer, "ssrLoadModule">,
42
+ ): Promise<HyacinePluginSystemConfig | null> {
43
+ const rootPath = fileURLToPath(root);
44
+ const configPath = findConfigFilePath(rootPath);
45
+
46
+ if (!configPath) {
47
+ console.warn("[hyacine-plugin] 未找到插件配置文件 (hyacine.plugin.ts 或 .mjs)");
48
+ return null;
49
+ }
50
+
51
+ try {
52
+ // dev 模式下优先使用 Vite 的 ssrLoadModule:
53
+ // 1) 可直接加载/转译 .ts 配置
54
+ // 2) 受 Vite module graph 管理,更符合 HMR 场景
55
+ // 3) 搭配 query 时间戳,确保每次都是“新模块 id”
56
+ let configModule: unknown;
57
+
58
+ if (viteServer) {
59
+ let moduleId = toViteFsPath(configPath);
60
+ if (bypassCache) {
61
+ moduleId = `${moduleId}?t=${Date.now()}`;
62
+ }
63
+ configModule = await viteServer.ssrLoadModule(moduleId);
64
+ } else {
65
+ // 使用原生动态导入加载配置文件(构建模式 / 无 Vite server 时)
66
+ let configUrl = pathToFileURL(configPath).href;
67
+ if (bypassCache) {
68
+ configUrl = `${configUrl}?t=${Date.now()}`;
69
+ }
70
+ configModule = await import(/* @vite-ignore */ configUrl);
71
+ }
72
+
73
+ const moduleAsRecord = configModule as Record<string, unknown>;
74
+ const config = moduleAsRecord.default as HyacinePluginSystemConfig;
75
+
76
+ if (!config || !config.plugins) {
77
+ console.warn("[hyacine-plugin] 配置文件格式无效,缺少 plugins 字段");
78
+ return null;
79
+ }
80
+
81
+ return config;
82
+ } catch (error) {
83
+ console.error("[hyacine-plugin] 加载配置文件失败:", error);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * 创建处理 runtime 虚拟模块的 Vite plugin
90
+ */
91
+ function createHyacineVitePlugin(
92
+ configGetter: () => HyacinePluginSystemConfig | null,
93
+ manifestsGetter: () => PluginManifest[] | null,
94
+ ): VitePlugin {
95
+ return {
96
+ name: "vite-plugin-hyacine-virtual",
97
+ enforce: "pre", // 确保在其他 plugins 之前执行
98
+
99
+ resolveId(id: string) {
100
+ // Runtime 虚拟模块
101
+ if (id === RUNTIME_VIRTUAL_ID) {
102
+ return RUNTIME_RESOLVED_ID;
103
+ }
104
+
105
+ // Astro 组件虚拟模块
106
+ if (id.startsWith(ASTRO_VIRTUAL_ID_PREFIX)) {
107
+ const injectPoint = id.slice(ASTRO_VIRTUAL_ID_PREFIX.length);
108
+ // 保留 .astro 扩展名,让 Astro 编译器识别
109
+ return `${ASTRO_RESOLVED_ID_PREFIX}${injectPoint}.astro`;
110
+ }
111
+
112
+ return null;
113
+ },
114
+
115
+ load(id: string) {
116
+ const config = configGetter();
117
+ const manifests = manifestsGetter();
118
+
119
+ if (!config || !manifests) {
120
+ return null;
121
+ }
122
+
123
+ // 加载 runtime 虚拟模块
124
+ if (id === RUNTIME_RESOLVED_ID) {
125
+ const runtimeEntries = collectRuntimeOnlyEntries(manifests);
126
+ return generateTempRuntimeFile(runtimeEntries, config.injectPoints);
127
+ }
128
+
129
+ // 加载 .astro 虚拟模块
130
+ if (id.startsWith(ASTRO_RESOLVED_ID_PREFIX) && id.endsWith(".astro")) {
131
+ const injectPoint = id.slice(ASTRO_RESOLVED_ID_PREFIX.length).replace(".astro", "");
132
+
133
+ const groups = groupEntriesByInjectPoint(manifests);
134
+ const entries = groups.get(injectPoint);
135
+
136
+ if (entries) {
137
+ // 返回 Astro 源码,让 Astro 的 Vite plugin 继续处理
138
+ return generateTempAstroTemplateFile(entries);
139
+ }
140
+ }
141
+
142
+ return null;
143
+ },
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Hyacine 插件系统 Astro 集成
149
+ */
150
+ export default function hyacinePlugin(): AstroIntegration {
151
+ let rootUrl: URL;
152
+ let rootPath: string;
153
+ const configFiles = CONFIG_FILE_NAMES;
154
+
155
+ // 缓存配置和 manifests 供虚拟模块使用
156
+ let cachedConfig: HyacinePluginSystemConfig | null = null;
157
+ let cachedManifests: PluginManifest[] | null = null;
158
+
159
+ // 保存 change 监听器的引用,用于清理
160
+ let changeListener: ((filePath: string) => Promise<void>) | null = null;
161
+
162
+ return {
163
+ name: "hyacine-plugin",
164
+ hooks: {
165
+ "astro:config:setup": async ({ config, injectScript, updateConfig }) => {
166
+ rootUrl = config.root;
167
+ rootPath = fileURLToPath(config.root);
168
+
169
+ // 读取并缓存配置
170
+ const pluginConfig = await loadPluginConfig(rootUrl, false);
171
+ if (pluginConfig) {
172
+ cachedConfig = pluginConfig;
173
+ cachedManifests = getPluginManifests(pluginConfig);
174
+ }
175
+
176
+ // 注入 Vite plugin 处理虚拟模块
177
+ updateConfig({
178
+ vite: {
179
+ plugins: [
180
+ createHyacineVitePlugin(
181
+ () => cachedConfig,
182
+ () => cachedManifests,
183
+ ) as any, // 类型断言解决 Vite 版本不一致问题
184
+ ],
185
+ },
186
+ });
187
+
188
+ // 使用虚拟模块路径注入 runtime
189
+ injectScript("page", `import "${RUNTIME_VIRTUAL_ID}";`);
190
+ },
191
+
192
+ "astro:server:setup": async ({ server, logger }) => {
193
+ server.watcher.setMaxListeners(20); // 增加监听器上限,避免警告
194
+ // 如果已有旧的监听器,先移除
195
+ if (changeListener) {
196
+ server.watcher.off("change", changeListener);
197
+ }
198
+
199
+ // 手动将配置文件加入 Vite watcher,避免使用 addWatchFile 导致这整个 server 重启
200
+ for (const fileName of configFiles) {
201
+ const configPath = join(rootPath, fileName);
202
+ if (existsSync(configPath)) {
203
+ server.watcher.add(configPath);
204
+ logger.info(`[hyacine-plugin] 监听配置文件 (HMR): ${fileName}`);
205
+ }
206
+ }
207
+
208
+ // 创建新的监听器并保存引用
209
+ changeListener = async (filePath: string) => {
210
+ const isPluginConfig = configFiles.some((name) => filePath.endsWith(name));
211
+
212
+ if (isPluginConfig) {
213
+ logger.info("[hyacine-plugin] 检测到配置变化,重新加载插件配置...");
214
+
215
+ try {
216
+ // 重新加载配置(绕过缓存)
217
+ const config = await loadPluginConfig(rootUrl, true, server);
218
+ if (config) {
219
+ // 更新缓存
220
+ cachedConfig = config;
221
+ cachedManifests = getPluginManifests(config);
222
+
223
+ // 使所有虚拟模块失效
224
+ const virtualModuleIds = [
225
+ RUNTIME_RESOLVED_ID,
226
+ // 所有 inject point 虚拟模块
227
+ ...Object.keys(config.injectPoints).map(
228
+ (injectPoint) => `${ASTRO_RESOLVED_ID_PREFIX}${injectPoint}.astro`,
229
+ ),
230
+ ];
231
+
232
+ for (const moduleId of virtualModuleIds) {
233
+ const mod = server.moduleGraph.getModuleById(moduleId);
234
+ if (mod) {
235
+ server.moduleGraph.invalidateModule(mod);
236
+ }
237
+ }
238
+
239
+ logger.info("[hyacine-plugin] 已使所有虚拟模块失效");
240
+
241
+ // 触发完整页面重载(HMR)
242
+ server.ws.send({
243
+ type: "full-reload",
244
+ path: "*",
245
+ });
246
+
247
+ logger.info("[hyacine-plugin] 配置已重新加载,页面即将刷新");
248
+ }
249
+ } catch (error) {
250
+ logger.error(
251
+ `[hyacine-plugin] 重新加载失败: ${error instanceof Error ? error.message : String(error)}`,
252
+ );
253
+ }
254
+ }
255
+ };
256
+
257
+ // 监听配置文件变化,触发重新生成
258
+ server.watcher.on("change", changeListener);
259
+ },
260
+
261
+ "astro:server:start": async ({ logger }) => {
262
+ if (!cachedConfig || !cachedManifests) {
263
+ logger.warn("[hyacine-plugin] 未找到有效的插件配置");
264
+ return;
265
+ }
266
+
267
+ logger.info(`[hyacine-plugin] 开发模式:已加载 ${cachedManifests.length} 个插件`);
268
+ },
269
+
270
+ "astro:build:start": async ({ logger }) => {
271
+ // 构建时虚拟模块会自动被 Vite 处理,无需手动生成文件
272
+ if (cachedConfig && cachedManifests) {
273
+ logger.info(`[hyacine-plugin] 构建模式:${cachedManifests.length} 个插件已加载`);
274
+ }
275
+ },
276
+ },
277
+ };
278
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export { default as hyacinePlugin } from "./hyacine-plugin";
@@ -0,0 +1,38 @@
1
+ /**
2
+ * 虚拟模块类型定义
3
+ * 为 Hyacine 插件系统的虚拟模块提供 TypeScript 类型支持
4
+ */
5
+
6
+ /**
7
+ * Runtime 虚拟模块
8
+ * 负责初始化全局注入点映射和执行 runtime-only 插件
9
+ *
10
+ * @remarks
11
+ * 该模块会自动注入到每个页面,无需手动导入
12
+ * 执行后会设置 globalThis.__HYACINE_INJECT_POINTS__
13
+ */
14
+ declare module "virtual:hyacine/runtime" {
15
+ // Runtime module 是 side-effect only,无导出
16
+ }
17
+ /**
18
+ * Inject Point 虚拟模块
19
+ * 每个注入点对应一个虚拟 Astro 组件
20
+ *
21
+ * @remarks
22
+ * 使用方式:
23
+ * ```astro
24
+ * ---
25
+ * import Footer from "virtual:hyacine/inject/footer";
26
+ * ---
27
+ * <Footer />
28
+ * ```
29
+ *
30
+ * 替代原有的文件系统路径:
31
+ * - 旧:`import Footer from "/generated/hyacine/footer.astro"`
32
+ * - 新:`import Footer from "virtual:hyacine/inject/footer"`
33
+ */
34
+ declare module "virtual:hyacine/inject/*" {
35
+ import type { AstroComponentFactory } from "astro/runtime/server/index.js";
36
+ const component: AstroComponentFactory;
37
+ export default component;
38
+ }