@ubean/islands 0.6.0 → 0.6.1-beta.2

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/dist/runtime.d.ts CHANGED
@@ -58,6 +58,8 @@ export declare function hydrateIslands(options?: HydrateIslandsOptions): void;
58
58
  export declare const SERVER_COMPONENT_ENDPOINT = "/__server-component";
59
59
  /**
60
60
  * 注册服务端组件到全局注册表 (SSR 构建中由 `defineServerIsland` 自动调用)。
61
+ *
62
+ * 同名重复注册会覆盖 —— HMR 重新求值页面模块时因此能拿到最新组件引用。
61
63
  */
62
64
  export declare function registerServerComponent(path: string, component: Component): void;
63
65
  /**
package/dist/runtime.js CHANGED
@@ -138,9 +138,73 @@ function hydrateIslands(options = {}) {
138
138
  } else doHydrate();
139
139
  }
140
140
  }
141
+ /**
142
+ * 把注册表里的 value 归一化成「组件或组件 Promise」。
143
+ *
144
+ * 注册表的 value 有两种合法形态,**而且都是函数**,不能靠 `typeof` 区分:
145
+ * - 惰性 loader:`() => import('…')` —— 必须**调用**才能拿到组件
146
+ * - 函数式组件:`(props, ctx) => VNode` —— 必须原样交给 Vue
147
+ *
148
+ * 判据是「调用后的返回值是不是 thenable」。
149
+ *
150
+ * 这修复了一个**静默**错误:`virtual:ubean-islands-registry` 导出的是惰性
151
+ * loader,而此前 `Promise.resolve(loader)` 不会调用 loader —— 它只是把
152
+ * loader 本身当成组件交给 Vue。Vue 于是把 `import()` 的 Promise 渲染成文本
153
+ * 节点 `[object Promise]`,而 `hydrateIsland` 又提前打上
154
+ * `data-hydrated="true"`,最终表现为「水合标记已完成、内容却是
155
+ * `[object Promise]`」——断言 `data-hydrated` 的测试会误判为通过,
156
+ * 只有真正读 DOM 内容的测试才能发现(靠 30s 超时暴露)。
157
+ */
158
+ function resolveComponentValue(value) {
159
+ if (typeof value !== "function") return unwrapComponentModule(value);
160
+ const asRecord = value;
161
+ if (asRecord.render || asRecord.setup) return value;
162
+ try {
163
+ const maybe = unwrapComponentModule(value());
164
+ if (maybe && typeof maybe.then === "function") return maybe.then((unwrapped) => unwrapComponentModule(unwrapped));
165
+ if (looksLikeComponent(maybe)) return maybe;
166
+ } catch {}
167
+ return value;
168
+ }
169
+ /**
170
+ * 判断一次试探性调用的返回值「像不像组件」。
171
+ *
172
+ * 关键是排除 VNode:函数式组件 `(props, ctx) => VNode` 被以 `undefined` props
173
+ * 调用时返回的正是 VNode,若把它当成组件返回,就会把渲染结果当组件二次挂载。
174
+ * Vue 的 VNode 带 `__v_isVNode` 标记,用它做否定判据。
175
+ */
176
+ function looksLikeComponent(value) {
177
+ if (typeof value === "function") return true;
178
+ if (!value || typeof value !== "object") return false;
179
+ const record = value;
180
+ if (record.__v_isVNode) return false;
181
+ return "render" in record || "setup" in record || "__name" in record || "template" in record;
182
+ }
183
+ /**
184
+ * 拆掉 ESM 模块命名空间,取出 `default` 导出。
185
+ *
186
+ * 惰性 loader 的真实形态是 `() => import('/src/components/X.vue')`,它 resolve 的
187
+ * **不是组件本身**,而是模块命名空间 `{ default: Component }`。若把命名空间当组件
188
+ * 交给 Vue,会得到 `[Vue warn] Component is missing template or render function: Module`
189
+ * —— 内容依然是空的。这里统一在「调用后」和「Promise resolve 后」两处拆包。
190
+ *
191
+ * 只认带 `default` 且**没有** `__esModule` 标记之外其它组件特征的普通对象,
192
+ * 避免误拆 `defineAsyncComponent` 之类的合法组件对象。
193
+ */
194
+ function unwrapComponentModule(value) {
195
+ if (!value || typeof value !== "object") return value;
196
+ const record = value;
197
+ if (record.__v_isVNode || record.render || record.setup || record.template) return value;
198
+ if (!("default" in record)) return value;
199
+ const inner = record.default;
200
+ return looksLikeComponent(inner) ? inner : value;
201
+ }
141
202
  function resolveComponent(name, components, getComponent) {
142
- if (components[name]) return components[name];
143
- if (getComponent) return getComponent(name);
203
+ if (components[name]) return resolveComponentValue(components[name]);
204
+ if (getComponent) {
205
+ const resolved = getComponent(name);
206
+ return resolved ? resolveComponentValue(resolved) : null;
207
+ }
144
208
  const registered = Object.keys(components);
145
209
  console.warn(`[ubean:islands] Island component "${name}" not found in registry.\nPossible causes:\n 1. Component is globally registered or dynamically imported — pass it via hydrateIslands({ components: { ${name}: YourComp } })\n 2. Component name mismatch between template tag and import\n 3. Component is auto-imported by unplugin-vue-components (no static import → not in auto-registry)\nRegistered components: ${registered.length > 0 ? registered.join(", ") : "(none)"}`);
146
210
  return null;
@@ -159,24 +223,43 @@ const SERVER_COMPONENT_ENDPOINT = "/__server-component";
159
223
  *
160
224
  * 中间件 `createServerComponentMiddleware()` 通过 `getServerComponent(path)` 取出
161
225
  * 组件,用 `renderToString(h(Comp, props))` 重新渲染并返回 HTML。
226
+ *
227
+ * **真·进程单例(挂在 `globalThis` 上)** —— 不能用模块级 `Map`:dev 的 SSR 图会把本文件
228
+ * 内联进 Vite 预构建 chunk(实测 `form-action-*.js`),而 `createUbeanApp()` 由 `@ubean/app`
229
+ * 的产物静态 `import '@ubean/islands/server'` 走 Node 原生解析取到**另一份** `dist/runtime.js`。
230
+ * 模块级状态只在同一个模块实例内共享,于是「SSR 渲染时注册」与「`POST /__server-component`
231
+ * 查找」读的是两个 Map,表现为该端点恒 404 `Component not registered for path`。
232
+ *
233
+ * 同类先例:`@ubean/vue` 的 matcher 注册表(`packages/vue/src/matchers.ts`)与
234
+ * `@ubean/build` 的模块注册表都出于同一理由挂在 `globalThis` 上(AGENTS.md §8 #19)。
162
235
  */
163
- const serverComponentRegistry = /* @__PURE__ */ new Map();
236
+ const SERVER_COMPONENT_REGISTRY_KEY = "__ubean_server_component_registry__";
237
+ function getServerComponentRegistry() {
238
+ const store = globalThis;
239
+ const existing = store[SERVER_COMPONENT_REGISTRY_KEY];
240
+ if (existing instanceof Map) return existing;
241
+ const created = /* @__PURE__ */ new Map();
242
+ store[SERVER_COMPONENT_REGISTRY_KEY] = created;
243
+ return created;
244
+ }
164
245
  /**
165
246
  * 注册服务端组件到全局注册表 (SSR 构建中由 `defineServerIsland` 自动调用)。
247
+ *
248
+ * 同名重复注册会覆盖 —— HMR 重新求值页面模块时因此能拿到最新组件引用。
166
249
  */
167
250
  function registerServerComponent(path, component) {
168
- serverComponentRegistry.set(path, component);
251
+ getServerComponentRegistry().set(path, component);
169
252
  }
170
253
  /**
171
254
  * 从全局注册表取出服务端组件 (由 `createServerComponentMiddleware` 调用)。
172
255
  * 未注册时返回 `undefined`。
173
256
  */
174
257
  function getServerComponent(path) {
175
- return serverComponentRegistry.get(path);
258
+ return getServerComponentRegistry().get(path);
176
259
  }
177
260
  /** 清空注册表 (仅用于测试)。 */
178
261
  function _clearServerComponentRegistry() {
179
- serverComponentRegistry.clear();
262
+ getServerComponentRegistry().clear();
180
263
  }
181
264
  /**
182
265
  * 定义服务端 island — 将异步组件包裹在 `<Suspense>` 边界中,实现 Partial
package/dist/vite.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { legacyDirectiveToStrategy, strategyToLegacyDirective } from "./directive.js";
2
2
  import { readFileSync, readdirSync } from "node:fs";
3
- import { dirname, join, resolve } from "node:path";
3
+ import { dirname, join, resolve } from "pathe";
4
4
  //#region src/vite.ts
5
5
  /**
6
6
  * `v-client.*` 指令属性名(Phase 4: 唯一支持的客户端 island 语法)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/islands",
3
- "version": "0.6.0",
3
+ "version": "0.6.1-beta.2",
4
4
  "description": "Islands architecture for ubean (Vite plugin for client:load/idle/visible/media/only directives, bootstrap script, client hydrate runtime)",
5
5
  "files": [
6
6
  "dist"
@@ -36,18 +36,19 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@ubean/shared": "0.6.0",
40
- "hono": "4.13.12",
39
+ "@ubean/shared": "0.6.1-beta.2",
40
+ "hono": "4.13.13",
41
+ "pathe": "^2.0.3",
41
42
  "vue": "^3.5.43"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@types/node": "^26.6.4",
45
46
  "typescript": "npm:typescript-native-bridge@latest",
46
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0",
47
- "vite-plus": "1.0.0"
47
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0",
48
+ "vite-plus": "1.1.0"
48
49
  },
49
50
  "peerDependencies": {
50
- "vite": "npm:@voidzero-dev/vite-plus-core@1.0.0"
51
+ "vite": "npm:@voidzero-dev/vite-plus-core@1.1.0"
51
52
  },
52
53
  "peerDependenciesMeta": {
53
54
  "vite": {