@ubean/islands 0.1.12 → 0.2.0

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,149 @@
1
+ import { DirectiveBinding, ObjectDirective } from "vue";
2
+ //#region src/directive.d.ts
3
+ /**
4
+ * The five hydration strategies supported by the Islands directive.
5
+ *
6
+ * - `load`: Hydrate immediately on page load (default).
7
+ * - `idle`: Hydrate when the browser is idle (`requestIdleCallback`).
8
+ * - `visible`: Hydrate when the element scrolls into viewport (`IntersectionObserver`).
9
+ * - `media`: Hydrate when a CSS media query matches (`matchMedia`).
10
+ * - `only`: Skip SSR entirely — render placeholder during SSR, hydrate on client.
11
+ */
12
+ type ClientStrategy = 'load' | 'idle' | 'visible' | 'media' | 'only';
13
+ /**
14
+ * Modifiers for the `v-client` directive.
15
+ *
16
+ * Exactly one modifier should be active. If multiple are present, the first
17
+ * matching strategy in priority order (load → idle → visible → media → only)
18
+ * is used.
19
+ */
20
+ interface ClientDirectiveModifiers {
21
+ /** Hydrate immediately on page load. */
22
+ load?: boolean;
23
+ /** Hydrate when the browser is idle. */
24
+ idle?: boolean;
25
+ /** Hydrate when the element enters the viewport. */
26
+ visible?: boolean;
27
+ /** Hydrate when the media query (passed as directive value) matches. */
28
+ media?: boolean;
29
+ /** Skip SSR, render only on the client. */
30
+ only?: boolean;
31
+ }
32
+ /**
33
+ * The value passed to `v-client.media` — a CSS media query string.
34
+ *
35
+ * For other strategies, the value is `undefined` (not passed).
36
+ *
37
+ * @example
38
+ * ```vue
39
+ * <Comp v-client.media="'(max-width: 768px)'" />
40
+ * <Comp v-client.media="isMobile ? '(max-width: 768px)' : '(min-width: 769px)'" />
41
+ * ```
42
+ */
43
+ type ClientDirectiveValue = string | undefined;
44
+ /**
45
+ * The full directive binding for `v-client`.
46
+ *
47
+ * Provides type-safe access to modifiers and value in directive hooks.
48
+ * Modifiers are cast to `ClientDirectiveModifiers` at runtime since Vue's
49
+ * `DirectiveBinding` types them as `Record<string, boolean>`.
50
+ */
51
+ type ClientDirectiveBinding = DirectiveBinding<ClientDirectiveValue>;
52
+ /**
53
+ * The `v-client` directive type.
54
+ *
55
+ * Use this for type-safe registration on a Vue app:
56
+ * ```typescript
57
+ * app.directive('client', vClient);
58
+ * ```
59
+ */
60
+ type VClientDirective = ObjectDirective<HTMLElement, ClientDirectiveValue, ClientStrategy>;
61
+ /**
62
+ * Resolve the hydration strategy from directive modifiers.
63
+ *
64
+ * Priority: load → idle → visible → media → only.
65
+ * If no modifier is present, defaults to `'load'`.
66
+ */
67
+ declare function resolveClientStrategy(modifiers: ClientDirectiveModifiers): ClientStrategy;
68
+ /**
69
+ * Map a `ClientStrategy` to the legacy `ClientDirective` string.
70
+ *
71
+ * Used internally by the Vite plugin to maintain backward compatibility
72
+ * with the `<ubean-island data-directive="client:load">` format.
73
+ */
74
+ declare function strategyToLegacyDirective(strategy: ClientStrategy): string;
75
+ /**
76
+ * Parse a directive string (legacy `client:xxx` or new `v-client.xxx`) into
77
+ * a `ClientStrategy`.
78
+ *
79
+ * @internal Used by the Vite plugin to unify old and new syntax.
80
+ */
81
+ declare function legacyDirectiveToStrategy(directive: string): ClientStrategy | null;
82
+ /** Data attribute used to mark elements for the Islands hydration system. */
83
+ declare const CLIENT_DIRECTIVE_ATTR = "data-client-directive";
84
+ /** Data attribute for the media query (used with `v-client.media`). */
85
+ declare const CLIENT_MEDIA_ATTR = "data-media";
86
+ /** Data attribute marking a client-only element (skip SSR). */
87
+ declare const CLIENT_ONLY_ATTR = "data-client-only";
88
+ /**
89
+ * The `v-client` Vue custom directive.
90
+ *
91
+ * Registers on the Vue app via `app.directive('client', vClient)`.
92
+ *
93
+ * In SSR/Islands mode: the Vite plugin transforms `v-client.*` before the
94
+ * directive runs, so this code only executes in CSR-only scenarios.
95
+ *
96
+ * In CSR-only mode: the directive marks the element with data attributes
97
+ * so `hydrateIslands()` can discover it, and directly applies the hydration
98
+ * strategy (idle/visible/media) for immediate effect.
99
+ */
100
+ declare const vClient: VClientDirective;
101
+ /**
102
+ * Apply a hydration deferral strategy directly to a DOM element.
103
+ *
104
+ * Used by the runtime directive (CSR mode) and exported for advanced use
105
+ * cases (e.g. custom wrapper components).
106
+ *
107
+ * - `load`: No deferral (element is already mounted).
108
+ * - `idle`: Defers visibility until `requestIdleCallback`.
109
+ * - `visible`: Defers visibility until `IntersectionObserver` fires.
110
+ * - `media`: Defers visibility until `matchMedia` matches.
111
+ * - `only`: No deferral (element is already on client).
112
+ */
113
+ declare function applyStrategy(el: HTMLElement, strategy: ClientStrategy, mediaQuery?: string): void;
114
+ /**
115
+ * Clean up all strategy resources for an element.
116
+ *
117
+ * Exported for wrapper components that manage elements manually.
118
+ */
119
+ declare function cleanupStrategy(el: HTMLElement): void;
120
+ /**
121
+ * Augment Vue's type system to recognize `v-client` in templates.
122
+ *
123
+ * This provides IDE autocompletion and type checking for:
124
+ * - `v-client.load` / `v-client.idle` / `v-client.visible` /
125
+ * `v-client.media` / `v-client.only`
126
+ * - The directive value (string for media query)
127
+ *
128
+ * @internal This is automatically applied when the module is imported.
129
+ */
130
+ declare module 'vue' {
131
+ interface ComponentCustomProperties {
132
+ /**
133
+ * `v-client` directive — Islands hydration strategy.
134
+ *
135
+ * Modifiers: `.load`, `.idle`, `.visible`, `.media`, `.only`
136
+ * Value: `string` (media query, only for `.media`)
137
+ */
138
+ vClient: VClientDirective;
139
+ }
140
+ interface ComponentCustomOptions {
141
+ /**
142
+ * Register the `v-client` directive on this component's app.
143
+ * (Framework-level — users don't need to set this manually.)
144
+ */
145
+ clientDirective?: boolean;
146
+ }
147
+ }
148
+ //#endregion
149
+ export { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, ClientDirectiveBinding, ClientDirectiveModifiers, ClientDirectiveValue, ClientStrategy, VClientDirective, applyStrategy, cleanupStrategy, legacyDirectiveToStrategy, resolveClientStrategy, strategyToLegacyDirective, vClient };
@@ -0,0 +1,158 @@
1
+ //#region src/directive.ts
2
+ /**
3
+ * Resolve the hydration strategy from directive modifiers.
4
+ *
5
+ * Priority: load → idle → visible → media → only.
6
+ * If no modifier is present, defaults to `'load'`.
7
+ */
8
+ function resolveClientStrategy(modifiers) {
9
+ if (modifiers.load) return "load";
10
+ if (modifiers.idle) return "idle";
11
+ if (modifiers.visible) return "visible";
12
+ if (modifiers.media) return "media";
13
+ if (modifiers.only) return "only";
14
+ return "load";
15
+ }
16
+ /**
17
+ * Map a `ClientStrategy` to the legacy `ClientDirective` string.
18
+ *
19
+ * Used internally by the Vite plugin to maintain backward compatibility
20
+ * with the `<ubean-island data-directive="client:load">` format.
21
+ */
22
+ function strategyToLegacyDirective(strategy) {
23
+ return `client:${strategy}`;
24
+ }
25
+ /**
26
+ * Parse a directive string (legacy `client:xxx` or new `v-client.xxx`) into
27
+ * a `ClientStrategy`.
28
+ *
29
+ * @internal Used by the Vite plugin to unify old and new syntax.
30
+ */
31
+ function legacyDirectiveToStrategy(directive) {
32
+ const match = directive.match(/^(?:client:|v-client\.)(load|idle|visible|media|only)$/);
33
+ return match ? match[1] : null;
34
+ }
35
+ /** Data attribute used to mark elements for the Islands hydration system. */
36
+ const CLIENT_DIRECTIVE_ATTR = "data-client-directive";
37
+ /** Data attribute for the media query (used with `v-client.media`). */
38
+ const CLIENT_MEDIA_ATTR = "data-media";
39
+ /** Data attribute marking a client-only element (skip SSR). */
40
+ const CLIENT_ONLY_ATTR = "data-client-only";
41
+ /** WeakMap for storing cleanup functions per element. */
42
+ const cleanupMap = /* @__PURE__ */ new WeakMap();
43
+ /**
44
+ * The `v-client` Vue custom directive.
45
+ *
46
+ * Registers on the Vue app via `app.directive('client', vClient)`.
47
+ *
48
+ * In SSR/Islands mode: the Vite plugin transforms `v-client.*` before the
49
+ * directive runs, so this code only executes in CSR-only scenarios.
50
+ *
51
+ * In CSR-only mode: the directive marks the element with data attributes
52
+ * so `hydrateIslands()` can discover it, and directly applies the hydration
53
+ * strategy (idle/visible/media) for immediate effect.
54
+ */
55
+ const vClient = {
56
+ mounted(el, binding) {
57
+ const modifiers = binding.modifiers;
58
+ const strategy = resolveClientStrategy(modifiers);
59
+ const mediaQuery = typeof binding.value === "string" ? binding.value : void 0;
60
+ el.setAttribute(CLIENT_DIRECTIVE_ATTR, strategy);
61
+ if (strategy === "media" && mediaQuery) el.setAttribute(CLIENT_MEDIA_ATTR, mediaQuery);
62
+ if (strategy === "only") el.setAttribute(CLIENT_ONLY_ATTR, "true");
63
+ applyStrategy(el, strategy, mediaQuery);
64
+ },
65
+ unmounted(el) {
66
+ const cleanups = cleanupMap.get(el);
67
+ if (cleanups) {
68
+ for (const cleanup of cleanups) cleanup();
69
+ cleanupMap.delete(el);
70
+ }
71
+ el.removeAttribute(CLIENT_DIRECTIVE_ATTR);
72
+ el.removeAttribute(CLIENT_MEDIA_ATTR);
73
+ el.removeAttribute(CLIENT_ONLY_ATTR);
74
+ }
75
+ };
76
+ /**
77
+ * Apply a hydration deferral strategy directly to a DOM element.
78
+ *
79
+ * Used by the runtime directive (CSR mode) and exported for advanced use
80
+ * cases (e.g. custom wrapper components).
81
+ *
82
+ * - `load`: No deferral (element is already mounted).
83
+ * - `idle`: Defers visibility until `requestIdleCallback`.
84
+ * - `visible`: Defers visibility until `IntersectionObserver` fires.
85
+ * - `media`: Defers visibility until `matchMedia` matches.
86
+ * - `only`: No deferral (element is already on client).
87
+ */
88
+ function applyStrategy(el, strategy, mediaQuery) {
89
+ if (strategy === "load" || strategy === "only") return;
90
+ const cleanups = [];
91
+ if (strategy === "idle") {
92
+ const ric = globalThis.requestIdleCallback;
93
+ if (typeof ric === "function") {
94
+ const id = ric(() => {
95
+ el.removeAttribute("data-client-pending");
96
+ }, { timeout: 2e3 });
97
+ cleanups.push(() => {
98
+ const cancel = globalThis.cancelIdleCallback;
99
+ if (typeof cancel === "function") cancel(id);
100
+ });
101
+ } else {
102
+ const id = setTimeout(() => {
103
+ el.removeAttribute("data-client-pending");
104
+ }, 200);
105
+ cleanups.push(() => clearTimeout(id));
106
+ }
107
+ el.setAttribute("data-client-pending", "idle");
108
+ } else if (strategy === "visible") {
109
+ const IOCtor = globalThis.IntersectionObserver;
110
+ if (typeof IOCtor === "function") {
111
+ const io = new IOCtor((entries) => {
112
+ for (const entry of entries) if (entry.isIntersecting) {
113
+ io.disconnect();
114
+ el.removeAttribute("data-client-pending");
115
+ }
116
+ }, { rootMargin: "200px" });
117
+ io.observe(el);
118
+ cleanups.push(() => io.disconnect());
119
+ }
120
+ el.setAttribute("data-client-pending", "visible");
121
+ } else if (strategy === "media" && mediaQuery) {
122
+ const mql = globalThis.window?.matchMedia?.(mediaQuery);
123
+ if (mql) {
124
+ if (mql.matches) {} else {
125
+ const fn = (e) => {
126
+ if (e.matches) {
127
+ el.removeAttribute("data-client-pending");
128
+ if (mql.removeEventListener) mql.removeEventListener("change", fn);
129
+ else if (mql.removeListener) mql.removeListener(fn);
130
+ }
131
+ };
132
+ if (mql.addEventListener) mql.addEventListener("change", fn);
133
+ else if (mql.addListener) mql.addListener(fn);
134
+ cleanups.push(() => {
135
+ if (mql.removeEventListener) mql.removeEventListener("change", fn);
136
+ else if (mql.removeListener) mql.removeListener(fn);
137
+ });
138
+ el.setAttribute("data-client-pending", "media");
139
+ }
140
+ }
141
+ }
142
+ if (cleanups.length > 0) cleanupMap.set(el, cleanups);
143
+ }
144
+ /**
145
+ * Clean up all strategy resources for an element.
146
+ *
147
+ * Exported for wrapper components that manage elements manually.
148
+ */
149
+ function cleanupStrategy(el) {
150
+ const cleanups = cleanupMap.get(el);
151
+ if (cleanups) {
152
+ for (const cleanup of cleanups) cleanup();
153
+ cleanupMap.delete(el);
154
+ }
155
+ el.removeAttribute("data-client-pending");
156
+ }
157
+ //#endregion
158
+ export { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, applyStrategy, cleanupStrategy, legacyDirectiveToStrategy, resolveClientStrategy, strategyToLegacyDirective, vClient };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ISLANDS_REGISTRY_VIRTUAL_ID, IslandComponentEntry, IslandComponentMap, UbeanIslandsPluginOptions, collectIslandComponents, generateRegistryModule, parseScriptImports, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin } from "./vite.js";
1
+ import { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, ClientDirectiveBinding, ClientDirectiveModifiers, ClientDirectiveValue, ClientStrategy, VClientDirective, applyStrategy, cleanupStrategy, legacyDirectiveToStrategy, resolveClientStrategy, strategyToLegacyDirective, vClient } from "./directive.js";
2
+ import { ClientComponentPlaceholder, IslandOptions, IslandStrategy, SERVER_COMPONENT_ENDPOINT, ServerComponentStub, ServerIslandOptions, defineClientComponent, defineIsland, definePairedComponent, defineServerIsland, getServerComponent, registerServerComponent } from "./runtime.js";
2
3
  //#region src/types.d.ts
3
4
  type ClientDirective = 'client:load' | 'client:idle' | 'client:visible' | 'client:media' | 'client:only';
4
5
  interface IslandDefinition {
@@ -37,4 +38,4 @@ declare const hydrationStrategyMeta: Record<ClientDirective, {
37
38
  declare function getIslandsBootstrapScript(): string;
38
39
  declare function getIslandsClearScript(): string;
39
40
  //#endregion
40
- export { type ClientDirective, type ClientHydrationStrategy, ISLANDS_REGISTRY_VIRTUAL_ID, type IslandComponentEntry, type IslandComponentMap, type IslandDefinition, type IslandSsrOptions, type IslandsContext, type UbeanIslandsPluginOptions, collectIslandComponents, createIslandsContext, generateIslandPlaceholder, generateRegistryModule, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, hydrationStrategyMeta, parseScriptImports, registerIsland, renderIslandPlaceholder, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin };
41
+ export { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, ClientComponentPlaceholder, type ClientDirective, type ClientDirectiveBinding, type ClientDirectiveModifiers, type ClientDirectiveValue, type ClientHydrationStrategy, type ClientStrategy, type IslandDefinition, type IslandOptions, type IslandSsrOptions, type IslandStrategy, type IslandsContext, SERVER_COMPONENT_ENDPOINT, ServerComponentStub, type ServerIslandOptions, type VClientDirective, applyStrategy, cleanupStrategy, createIslandsContext, defineClientComponent, defineIsland, definePairedComponent, defineServerIsland, generateIslandPlaceholder, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, getServerComponent, hydrationStrategyMeta, legacyDirectiveToStrategy, registerIsland, registerServerComponent, renderIslandPlaceholder, resolveClientStrategy, strategyToLegacyDirective, vClient };
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- import { ISLANDS_REGISTRY_VIRTUAL_ID, collectIslandComponents, generateRegistryModule, parseScriptImports, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin } from "./vite.js";
1
+ import { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, applyStrategy, cleanupStrategy, legacyDirectiveToStrategy, resolveClientStrategy, strategyToLegacyDirective, vClient } from "./directive.js";
2
+ import { ClientComponentPlaceholder, SERVER_COMPONENT_ENDPOINT, ServerComponentStub, defineClientComponent, defineIsland, definePairedComponent, defineServerIsland, getServerComponent, registerServerComponent } from "./runtime.js";
2
3
  //#region src/types.ts
3
4
  function createIslandsContext() {
4
5
  return {
@@ -65,4 +66,4 @@ function getIslandsClearScript() {
65
66
  return `(function(){function clearIslands(){var islands=document.querySelectorAll('ubean-island[data-island-id]');islands.forEach(function(el){if(el.getAttribute('data-directive')!=='client:only'){return}el.innerHTML='';el.setAttribute('data-cleared','true')})}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',clearIslands)}else{clearIslands()}})();`;
66
67
  }
67
68
  //#endregion
68
- export { ISLANDS_REGISTRY_VIRTUAL_ID, collectIslandComponents, createIslandsContext, generateIslandPlaceholder, generateRegistryModule, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, hydrationStrategyMeta, parseScriptImports, registerIsland, renderIslandPlaceholder, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin };
69
+ export { CLIENT_DIRECTIVE_ATTR, CLIENT_MEDIA_ATTR, CLIENT_ONLY_ATTR, ClientComponentPlaceholder, SERVER_COMPONENT_ENDPOINT, ServerComponentStub, applyStrategy, cleanupStrategy, createIslandsContext, defineClientComponent, defineIsland, definePairedComponent, defineServerIsland, generateIslandPlaceholder, getIslandsBootstrapScript, getIslandsClearScript, getIslandsScript, getServerComponent, hydrationStrategyMeta, legacyDirectiveToStrategy, registerIsland, registerServerComponent, renderIslandPlaceholder, resolveClientStrategy, strategyToLegacyDirective, vClient };
package/dist/runtime.d.ts CHANGED
@@ -4,6 +4,7 @@ interface DomElement {
4
4
  getAttribute(name: string): string | null;
5
5
  setAttribute(name: string, value: string): void;
6
6
  hasAttribute(name: string): boolean;
7
+ innerHTML: string;
7
8
  }
8
9
  interface NodeListOf<T> {
9
10
  forEach(callback: (value: T, key: number, parent: NodeListOf<T>) => void): void;
@@ -31,5 +32,307 @@ interface HydrateIslandsOptions extends IslandHydrateOptions {
31
32
  components?: Record<string, Component | (() => Promise<Component>)>;
32
33
  }
33
34
  declare function hydrateIslands(options?: HydrateIslandsOptions): void;
35
+ /**
36
+ * `POST /__server-component` 端点 — 接收 `{ path, props }`,在服务端用注册表中的
37
+ * 组件重新渲染并返回 HTML 片段。由 `createServerComponentMiddleware()` 处理。
38
+ */
39
+ declare const SERVER_COMPONENT_ENDPOINT = "/__server-component";
40
+ /**
41
+ * 注册服务端组件到全局注册表 (SSR 构建中由 `defineServerIsland` 自动调用)。
42
+ */
43
+ declare function registerServerComponent(path: string, component: Component): void;
44
+ /**
45
+ * 从全局注册表取出服务端组件 (由 `createServerComponentMiddleware` 调用)。
46
+ * 未注册时返回 `undefined`。
47
+ */
48
+ declare function getServerComponent(path: string): Component | undefined;
49
+ /** 清空注册表 (仅用于测试)。 */
50
+ declare function _clearServerComponentRegistry(): void;
51
+ /**
52
+ * `defineServerIsland` 的选项。
53
+ */
54
+ interface ServerIslandOptions {
55
+ /**
56
+ * Suspense fallback 内容。
57
+ *
58
+ * - 字符串:作为静态文本渲染(适合简单 loading 提示)
59
+ * - Vue 组件:作为 fallback slot 渲染(可包含动态内容)
60
+ * - 未提供:渲染空 `<ubean-defer-fallback>` 占位元素(与 PPR 静态壳默认行为一致)
61
+ *
62
+ * 在 PPR 模式下,预渲染(SSG)阶段仅渲染 fallback(生成静态壳);
63
+ * 流式 SSR 阶段 fallback 先输出,异步组件解析后通过 Suspense 边界流式输出。
64
+ */
65
+ fallback?: Component | string;
66
+ /**
67
+ * Task 9.4: 是否在 props 变化时重新请求服务端组件 HTML 并替换 DOM。
68
+ *
69
+ * - `false` (默认): 仅 SSR 渲染一次,客户端水合后不再请求服务端。
70
+ * - `true`: 客户端 `onMounted` 后立即请求一次,并 `watch` props 变化重新请求,
71
+ * 用返回的 HTML 替换容器 `innerHTML`。SSR 端会将组件注册到全局注册表供中间件查找。
72
+ *
73
+ * 需配合 Vite 插件自动注入的第 3 参数 (组件绝对路径) 使用。当 `true` 但未提供
74
+ * 路径时 (例如手动调用未走 Vite 插件),退化为 `false` 行为。
75
+ */
76
+ rerenderOnPropsChange?: boolean;
77
+ }
78
+ /**
79
+ * 定义服务端 island — 将异步组件包裹在 `<Suspense>` 边界中,实现 Partial
80
+ * Prerendering / Server Islands 模式。
81
+ *
82
+ * 替代旧版 `server:defer` 编译时指令。对齐 Next.js 16 PPR / Astro 5
83
+ * `server:defer` 语义。
84
+ *
85
+ * ## 工作机制
86
+ *
87
+ * - **预渲染(SSG)阶段**:仅渲染 fallback(生成静态壳)
88
+ * - **流式 SSR 阶段**:fallback 先输出,异步组件解析后通过 Suspense 边界流式输出
89
+ * - **客户端**:Suspense 边界保持,异步组件解析后自动替换 fallback
90
+ *
91
+ * 传入的 `Component` 必须是异步的(`async setup()` 或 `defineAsyncComponent`)
92
+ * 才能触发 Suspense 流式行为;同步组件会立即解析,Suspense 退化为透明包装。
93
+ *
94
+ * ## Task 9.4: Props 重渲染
95
+ *
96
+ * 当 `options.rerenderOnPropsChange: true` 且 Vite 插件注入了组件路径 (第 3 参数):
97
+ *
98
+ * - **SSR**: `defineServerIsland` 将组件注册到全局注册表 (`registerServerComponent`),
99
+ * 供 `POST /__server-component` 中间件查找。SSR 渲染输出与不带此选项时一致。
100
+ * - **客户端**: 包装组件外层渲染 `<ubean-server-island>` 容器 (带 ref),内部仍是
101
+ * `<Suspense><Component /></Suspense>` (client 构建中 `Component` 是 stub)。
102
+ * `onMounted` 后立即 `POST {path, props}` 到 `/__server-component`,用返回的 HTML
103
+ * 替换容器 `innerHTML`;`watch(attrs)` 在 props 变化时重复此流程。
104
+ *
105
+ * ## Props/Slots 透传
106
+ *
107
+ * 包装组件设置 `inheritAttrs: false`,将所有 attrs(含 props)和 slots
108
+ * 透传给内部 `Component`,使用方式与直接渲染 `Component` 一致:
109
+ *
110
+ * ```vue
111
+ * <DashboardIsland :userId="123" #header="slotProps">...</DashboardIsland>
112
+ * ```
113
+ *
114
+ * 等价于:
115
+ *
116
+ * ```vue
117
+ * <Suspense>
118
+ * <template #fallback><!-- fallback 内容 --></template>
119
+ * <Dashboard :userId="123" #header="slotProps">...</Dashboard>
120
+ * </Suspense>
121
+ * ```
122
+ *
123
+ * ## 用法示例
124
+ *
125
+ * ```ts
126
+ * import { defineServerIsland, h } from 'ubean';
127
+ * import Dashboard from './Dashboard.vue';
128
+ *
129
+ * // 1. 字符串 fallback
130
+ * const DashboardIsland = defineServerIsland(Dashboard, {
131
+ * fallback: 'Loading dashboard...'
132
+ * });
133
+ *
134
+ * // 2. 组件 fallback
135
+ * const DashboardIsland = defineServerIsland(Dashboard, {
136
+ * fallback: () => h('div', { class: 'spinner' }, 'Loading...')
137
+ * });
138
+ *
139
+ * // 3. 默认占位(fallback 未提供时渲染 <ubean-defer-fallback/>)
140
+ * const DashboardIsland = defineServerIsland(Dashboard);
141
+ *
142
+ * // 4. Task 9.4: props 变化时重渲染 (Vite 插件会自动注入第 3 参数)
143
+ * const DashboardIsland = defineServerIsland(Dashboard, {
144
+ * rerenderOnPropsChange: true
145
+ * });
146
+ * ```
147
+ *
148
+ * 在 SFC 中使用包装后的组件:
149
+ *
150
+ * ```vue
151
+ * <script setup>
152
+ * import { defineServerIsland } from 'ubean';
153
+ * import Dashboard from './Dashboard.vue';
154
+ * const DashboardIsland = defineServerIsland(Dashboard, {
155
+ * fallback: 'Loading...'
156
+ * });
157
+ * </script>
158
+ *
159
+ * <template>
160
+ * <DashboardIsland :userId="123" />
161
+ * </template>
162
+ * ```
163
+ *
164
+ * @param Component 服务端组件 (通常是 `.server.vue` 导入)
165
+ * @param options 选项
166
+ * @param __serverComponentPath 组件绝对路径 (由 Vite 插件自动注入,勿手动传)
167
+ */
168
+ declare function defineServerIsland(Component: Component, options?: ServerIslandOptions, __serverComponentPath?: string): Component;
169
+ /**
170
+ * 客户端 island 水合策略(与 `v-client.*` 的 modifier 一一对应)。
171
+ */
172
+ type IslandStrategy = 'load' | 'idle' | 'visible' | 'media' | 'only';
173
+ /**
174
+ * `defineIsland` 的选项。
175
+ */
176
+ interface IslandOptions {
177
+ /**
178
+ * 媒体查询字符串(仅 `strategy: 'media'` 时使用)。
179
+ *
180
+ * 与 `v-client.media="'(max-width: 768px)'"` 中的字符串值等价。
181
+ */
182
+ mediaQuery?: string;
183
+ /**
184
+ * 静态 props,在 SSR 阶段序列化到 `<ubean-island data-props="...">`。
185
+ *
186
+ * 未提供时,所有 attrs 都会作为 props 透传给内部组件。
187
+ * (与 `v-client.*` 在模板上接收 attrs 的行为一致。)
188
+ */
189
+ props?: Record<string, unknown>;
190
+ }
191
+ /**
192
+ * 定义客户端 island — 将组件包装为延迟水合的 island。
193
+ *
194
+ * 这是 `v-client.*` Vue 指令的运行时替代方案,适用于编程式场景
195
+ * (如动态构造 island、在 `.ts` 文件中定义 island、或需要参数化的场景)。
196
+ *
197
+ * **注意**:模板中的 `v-client.*` 指令语法仍然有效,由 Vite 插件
198
+ * (`ubean:islands`)在编译时转换为 `<ubean-island>` 占位元素;
199
+ * `defineIsland` 仅为运行时编程式使用提供,不会替代 Vite 插件的转换逻辑。
200
+ *
201
+ * ## 工作机制
202
+ *
203
+ * - **SSR 阶段**:渲染 `<ubean-island>` 占位元素,包含 `data-island-id`、
204
+ * `data-component`、`data-directive`、`data-props`、`data-media` 属性
205
+ * (与 `v-client.*` 转换后的输出格式完全一致)。
206
+ * - **客户端**:由 `hydrateIslands()` 根据 `data-directive` 选择水合策略
207
+ * (load/idle/visible/media/only),在合适时机挂载组件。
208
+ *
209
+ * ## 用法示例
210
+ *
211
+ * ```ts
212
+ * import { defineIsland } from 'ubean';
213
+ * import Counter from './Counter.vue';
214
+ *
215
+ * // 1. 立即水合(等价于 v-client.load)
216
+ * const CounterIsland = defineIsland(Counter, 'load');
217
+ *
218
+ * // 2. 空闲时水合(等价于 v-client.idle)
219
+ * const HeavyChartIsland = defineIsland(HeavyChart, 'idle');
220
+ *
221
+ * // 3. 进入视口时水合(等价于 v-client.visible)
222
+ * const LazyMapIsland = defineIsland(LazyMap, 'visible');
223
+ *
224
+ * // 4. 媒体查询匹配时水合(等价于 v-client.media)
225
+ * const MobileNavIsland = defineIsland(MobileNav, 'media', {
226
+ * mediaQuery: '(max-width: 768px)'
227
+ * });
228
+ *
229
+ * // 5. 仅客户端渲染(等价于 v-client.only)
230
+ * const ClientOnlyWidgetIsland = defineIsland(ClientOnlyWidget, 'only');
231
+ * ```
232
+ *
233
+ * 在 SFC 中使用包装后的组件:
234
+ *
235
+ * ```vue
236
+ * <script setup>
237
+ * import { defineIsland } from 'ubean';
238
+ * import Counter from './Counter.vue';
239
+ * const CounterIsland = defineIsland(Counter, 'load');
240
+ * </script>
241
+ *
242
+ * <template>
243
+ * <CounterIsland :count="5" />
244
+ * </template>
245
+ * ```
246
+ */
247
+ declare function defineIsland(Component: Component, strategy: IslandStrategy, options?: IslandOptions): Component;
248
+ /**
249
+ * `.server.vue` 组件在客户端 bundle 中的占位 stub (Task 9.1)。
250
+ *
251
+ * Vite 插件在 client 构建中将 `.server.vue` 的 import 重定向到虚拟 stub 模块,
252
+ * 该模块导出此组件。组件渲染一个空的 `<ubean-server-only>` 元素 —— SSR 已在
253
+ * 该元素内部渲染了完整 HTML,客户端 Vue 水合时匹配该元素但不触碰其子节点
254
+ * (SSR 模板通过 `v-once` 标记为静态内容),从而保留服务端渲染的 HTML 不被清除。
255
+ *
256
+ * 这保证了 `.server.vue` 组件的 JS 不会发送到客户端 —— 客户端只导入此 stub。
257
+ */
258
+ declare const ServerComponentStub: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
259
+ [key: string]: any;
260
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
261
+ /**
262
+ * `.client.vue` 组件在 SSR 构建中的通用占位符 (Task 9.2)。
263
+ *
264
+ * Vite 插件在 SSR 构建中将 `.client.vue` 的 import 重定向到虚拟模块,
265
+ * 该模块导出此组件。组件渲染 `<div data-client-only></div>` 占位符,
266
+ * 与客户端 `defineClientComponent` 初始渲染输出一致,确保水合无 mismatch。
267
+ *
268
+ * SSR 使用通用占位符(而非真实组件)避免了在服务端导入可能含浏览器 API
269
+ * 的 `.client.vue` 组件代码。
270
+ */
271
+ declare const ClientComponentPlaceholder: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
272
+ [key: string]: any;
273
+ }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
274
+ /**
275
+ * 定义客户端组件 —— `.client.vue` 在客户端构建中的包装器 (Task 9.2)。
276
+ *
277
+ * Vite 插件在 client 构建中为每个 `.client.vue` 生成虚拟包装模块:
278
+ * ```ts
279
+ * import RealComp from '/abs/path/Foo.client.vue';
280
+ * import { defineClientComponent } from '@ubean/islands/runtime';
281
+ * export default defineClientComponent(RealComp);
282
+ * ```
283
+ *
284
+ * ## 工作机制
285
+ *
286
+ * - **SSR**: 由 `ClientComponentPlaceholder` 替代,渲染 `<div data-client-only></div>`
287
+ * - **客户端初始渲染**: `isClient` 为 false,渲染相同的 `<div data-client-only></div>`,
288
+ * 与 SSR 输出匹配,水合无 mismatch
289
+ * - **客户端 `onMounted` 后**: `isClient` 变为 true,渲染真实组件,Vue 自动 patch 替换占位符
290
+ *
291
+ * 这种模式确保 `.client.vue` 组件只在客户端渲染,SSR 仅输出占位符,
292
+ * 且不依赖 islands 注册表 / `hydrateIslands()` 机制。
293
+ *
294
+ * ## 用法
295
+ *
296
+ * 通常由 Vite 插件自动生成包装模块,用户无需手动调用。如需编程式使用:
297
+ *
298
+ * ```ts
299
+ * import { defineClientComponent } from '@ubean/islands/runtime';
300
+ * import Widget from './Widget.client.vue';
301
+ * const WidgetClient = defineClientComponent(Widget);
302
+ * ```
303
+ */
304
+ declare function defineClientComponent(component: Component): Component;
305
+ /**
306
+ * 定义配对组件 — `Foo.vue` 同时存在 `.server.vue` + `.client.vue` 兄弟文件时,
307
+ * Vite 插件生成的虚拟包装模块调用此函数 (Task 9.3)。
308
+ *
309
+ * ## 工作机制
310
+ *
311
+ * - **SSR**: 配对 wrapper 模块直接 re-export `.server.vue`,根本不会调用本函数
312
+ * (见 `vite.ts` `load` 钩子 SSR 分支)。SSR 渲染真实服务端组件内容。
313
+ * - **客户端初始渲染**: `isClient` 为 false,渲染 `ServerComp` —— 但在客户端构建中
314
+ * `.server.vue` 已被重定向到 `ServerComponentStub`(渲染空的
315
+ * `<ubean-server-only>` 元素),与 SSR 输出的 `<ubean-server-only v-once>真实内容
316
+ * </ubean-server-only>` 标签匹配,Vue 水合时元素标签一致 (内部子节点因 `v-once`
317
+ * 标记为静态而被保留,虽有 mismatch 但 Vue 通常能容忍)。
318
+ * - **客户端 `onMounted` 后**: `isClient` 变为 true,渲染 `ClientComp` (真实客户端
319
+ * 组件),Vue 自动 patch 替换 stub 内容。
320
+ *
321
+ * ## 局限性
322
+ *
323
+ * SSR 渲染的 HTML 在客户端水合时不会被完美保留 —— Vue 会尝试 patch stub 的空
324
+ * `<ubean-server-only>` 与 SSR 输出的有内容版本,可能导致 SSR 内容被清除后再渲染
325
+ * 客户端组件。这是已知的折中 (与 React Server Components 的 hydration 流程类似)。
326
+ *
327
+ * 通常由 Vite 插件自动生成包装模块,用户无需手动调用。如需编程式使用:
328
+ *
329
+ * ```ts
330
+ * import { definePairedComponent } from '@ubean/islands/runtime';
331
+ * import ServerComp from './Foo.server.vue';
332
+ * import ClientComp from './Foo.client.vue';
333
+ * const Foo = definePairedComponent(ServerComp, ClientComp);
334
+ * ```
335
+ */
336
+ declare function definePairedComponent(ServerComp: Component, ClientComp: Component): Component;
34
337
  //#endregion
35
- export { HydrateIslandsOptions, IslandHydrateOptions, IslandRecord, collectIslands, hydrateIsland, hydrateIslands };
338
+ export { ClientComponentPlaceholder, HydrateIslandsOptions, IslandHydrateOptions, IslandOptions, IslandRecord, IslandStrategy, SERVER_COMPONENT_ENDPOINT, ServerComponentStub, ServerIslandOptions, _clearServerComponentRegistry, collectIslands, defineClientComponent, defineIsland, definePairedComponent, defineServerIsland, getServerComponent, hydrateIsland, hydrateIslands, registerServerComponent };