@ubean/islands 0.6.0-beta.1 → 0.6.0-beta.3

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
@@ -1,4 +1,4 @@
1
- import { App, Component } from "vue";
1
+ import { App, Component, VNode } from "vue";
2
2
  //#region src/runtime.d.ts
3
3
  interface DomElement {
4
4
  getAttribute(name: string): string | null;
@@ -274,20 +274,20 @@ export declare function defineIsland(Component: Component, strategy: IslandStrat
274
274
  *
275
275
  * 这保证了 `.server.vue` 组件的 JS 不会发送到客户端 —— 客户端只导入此 stub。
276
276
  */
277
- export declare const ServerComponentStub: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
277
+ export declare const ServerComponentStub: import("vue").DefineComponent<{}, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
278
278
  [key: string]: any;
279
279
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
280
280
  /**
281
281
  * `.client.vue` 组件在 SSR 构建中的通用占位符 (Task 9.2)。
282
282
  *
283
283
  * Vite 插件在 SSR 构建中将 `.client.vue` 的 import 重定向到虚拟模块,
284
- * 该模块导出此组件。组件渲染 `<div data-client-only></div>` 占位符,
284
+ * 该模块导出此组件。组件渲染 {@link clientOnlyPlaceholder}(`<!--client-only-->`),
285
285
  * 与客户端 `defineClientComponent` 初始渲染输出一致,确保水合无 mismatch。
286
286
  *
287
287
  * SSR 使用通用占位符(而非真实组件)避免了在服务端导入可能含浏览器 API
288
288
  * 的 `.client.vue` 组件代码。
289
289
  */
290
- export declare const ClientComponentPlaceholder: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
290
+ export declare const ClientComponentPlaceholder: import("vue").DefineComponent<{}, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
291
291
  [key: string]: any;
292
292
  }>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
293
293
  /**
@@ -302,9 +302,8 @@ export declare const ClientComponentPlaceholder: import("vue").DefineComponent<{
302
302
  *
303
303
  * ## 工作机制
304
304
  *
305
- * - **SSR**: 由 `ClientComponentPlaceholder` 替代,渲染 `<div data-client-only></div>`
306
- * - **客户端初始渲染**: `isClient` 为 false,渲染相同的 `<div data-client-only></div>`,
307
- * 与 SSR 输出匹配,水合无 mismatch
305
+ * - **SSR**: 由 `ClientComponentPlaceholder` 替代,渲染 `<!--client-only-->` 注释占位符
306
+ * - **客户端初始渲染**: `isClient` 为 false,渲染同一个注释占位符,与 SSR 输出匹配,水合无 mismatch
308
307
  * - **客户端 `onMounted` 后**: `isClient` 变为 true,渲染真实组件,Vue 自动 patch 替换占位符
309
308
  *
310
309
  * 这种模式确保 `.client.vue` 组件只在客户端渲染,SSR 仅输出占位符,
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Suspense, createApp, defineComponent, h, onMounted, ref, watch } from "vue";
1
+ import { Suspense, createApp, createCommentVNode, defineComponent, h, onMounted, ref, watch } from "vue";
2
2
  //#region src/runtime.ts
3
3
  const _global = globalThis;
4
4
  function decodeProps(raw) {
@@ -420,10 +420,25 @@ const ServerComponentStub = defineComponent({
420
420
  }
421
421
  });
422
422
  /**
423
+ * `.client.vue` 的占位符 vnode —— SSR 与客户端水合首帧共用同一形态。
424
+ *
425
+ * **是注释节点,不是元素**(这与 `@ubean/vue` 的 `<ClientOnly>` 一致,标记文本也相同)。
426
+ * 元素占位符(早先是 `<div data-client-only>`)在受限的父级里会产出**非法嵌套**:放进
427
+ * `<tr>` / `<table>` / `<ul>` / `<select>` 时会被 HTML 解析器提到容器外(表格里的 `<div>`
428
+ * 会被 foster-parent 到表格之前),DOM 与客户端 vnode 树随即错位 → 水合 mismatch;放进
429
+ * `<p>` 也会被自动闭合。注释在任何位置都合法、不占布局,因此没有这类上下文依赖。
430
+ *
431
+ * 代价是不能再对它写 CSS(`[data-client-only]` 选择器失效)。需要占位内容/撑高度时用
432
+ * `<ClientOnly>` 的 `#fallback` 插槽 —— 那是显式的、由使用者决定的占位内容。
433
+ */
434
+ function clientOnlyPlaceholder() {
435
+ return createCommentVNode("client-only");
436
+ }
437
+ /**
423
438
  * `.client.vue` 组件在 SSR 构建中的通用占位符 (Task 9.2)。
424
439
  *
425
440
  * Vite 插件在 SSR 构建中将 `.client.vue` 的 import 重定向到虚拟模块,
426
- * 该模块导出此组件。组件渲染 `<div data-client-only></div>` 占位符,
441
+ * 该模块导出此组件。组件渲染 {@link clientOnlyPlaceholder}(`<!--client-only-->`),
427
442
  * 与客户端 `defineClientComponent` 初始渲染输出一致,确保水合无 mismatch。
428
443
  *
429
444
  * SSR 使用通用占位符(而非真实组件)避免了在服务端导入可能含浏览器 API
@@ -432,7 +447,7 @@ const ServerComponentStub = defineComponent({
432
447
  const ClientComponentPlaceholder = defineComponent({
433
448
  name: "ClientComponentPlaceholder",
434
449
  setup() {
435
- return () => h("div", { "data-client-only": "" });
450
+ return () => clientOnlyPlaceholder();
436
451
  }
437
452
  });
438
453
  /**
@@ -447,9 +462,8 @@ const ClientComponentPlaceholder = defineComponent({
447
462
  *
448
463
  * ## 工作机制
449
464
  *
450
- * - **SSR**: 由 `ClientComponentPlaceholder` 替代,渲染 `<div data-client-only></div>`
451
- * - **客户端初始渲染**: `isClient` 为 false,渲染相同的 `<div data-client-only></div>`,
452
- * 与 SSR 输出匹配,水合无 mismatch
465
+ * - **SSR**: 由 `ClientComponentPlaceholder` 替代,渲染 `<!--client-only-->` 注释占位符
466
+ * - **客户端初始渲染**: `isClient` 为 false,渲染同一个注释占位符,与 SSR 输出匹配,水合无 mismatch
453
467
  * - **客户端 `onMounted` 后**: `isClient` 变为 true,渲染真实组件,Vue 自动 patch 替换占位符
454
468
  *
455
469
  * 这种模式确保 `.client.vue` 组件只在客户端渲染,SSR 仅输出占位符,
@@ -474,7 +488,7 @@ function defineClientComponent(component) {
474
488
  onMounted(() => {
475
489
  isClient.value = true;
476
490
  });
477
- return () => isClient.value ? h(component, attrs, slots) : h("div", { "data-client-only": "" });
491
+ return () => isClient.value ? h(component, attrs, slots) : clientOnlyPlaceholder();
478
492
  }
479
493
  });
480
494
  }
package/dist/vite.d.ts CHANGED
@@ -28,6 +28,34 @@ export declare const CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID = "virtual:ubean-cl
28
28
  * 真实文件不会被加载/转换)。
29
29
  */
30
30
  export declare function wrapServerComponentTemplate(code: string): string | null;
31
+ /**
32
+ * `.server.vue` 是否带 `<template>` 块。
33
+ *
34
+ * 用于把「包裹不了」的两种情况区分开:**没有 `<template>`**(render 函数 / `export { default }
35
+ * from …` 这类写法)必须报错,而**已经包裹过**(幂等)应当静默跳过。
36
+ *
37
+ * 为什么没有 `<template>` 就必须报错:客户端侧一律替换成 stub(渲染 `<ubean-server-only>`),
38
+ * 服务端侧的内容必须被包进**同名元素**才能在水合时对齐。没有 `<template>` 时服务端输出的是
39
+ * 组件自己的根元素,与 stub 的根标签不一致 —— 实测 Vue 报
40
+ * `Hydration node mismatch: rendered on server: JSHandle@node / expected on client: ubean-server-only`,
41
+ * 并且**把服务端渲染出来的内容清掉**(首屏有内容、水合后变空),比不渲染更糟。
42
+ */
43
+ export declare function hasServerComponentTemplate(code: string): boolean;
44
+ export interface RestrictedServerComponentUsage {
45
+ /** 受限父级标签(小写)。 */
46
+ parent: string;
47
+ /** 组件在模板里的写法(原样)。 */
48
+ tag: string;
49
+ }
50
+ /**
51
+ * 找出「直接作为受限父级子元素」使用的 `.server.vue` 组件(见
52
+ * {@link RESTRICTED_SERVER_COMPONENT_PARENTS})。
53
+ *
54
+ * 判据只看**静态可解析**的部分:标签名能在 `<script setup>` 的 import 映射里找到,且 import 路径
55
+ * 以 `.server.vue` 结尾。驼峰/连字符两种写法都认(`<ServerGreeting />` 与 `<server-greeting />`)。
56
+ * 通过 barrel 再导出、或运行时动态解析的组件看不出来 —— 宁可漏报也不误报。
57
+ */
58
+ export declare function findRestrictedServerComponentUsage(code: string, importMap: Map<string, string>): RestrictedServerComponentUsage[];
31
59
  /**
32
60
  * 一个 island 组件的注册条目。
33
61
  *
package/dist/vite.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { legacyDirectiveToStrategy, strategyToLegacyDirective } from "./directive.js";
2
- import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { readFileSync, readdirSync } from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  //#region src/vite.ts
5
5
  /**
@@ -46,6 +46,18 @@ function isClientComponentFile(id) {
46
46
  /** `.server.vue` 在 client 构建中的通用虚拟 stub 模块 ID */
47
47
  const SERVER_COMPONENT_STUB_VIRTUAL_ID = "virtual:ubean-server-component-stub";
48
48
  const SERVER_COMPONENT_STUB_RESOLVED_ID = `\0${SERVER_COMPONENT_STUB_VIRTUAL_ID}`;
49
+ /**
50
+ * `.server.vue` 在 client 构建中的**文件级** stub 模块 ID 前缀。
51
+ *
52
+ * 早期所有 `.server.vue` 都重定向到上面那个**共用**的 stub 模块,于是它们在客户端图里成了
53
+ * 同一个组件对象 —— `name` 全是 `ServerComponentStub`,`<KeepAlive include>`、
54
+ * `<component :is>`、DevTools 台账都失去区分度。改为按文件生成(与 `.client.vue` 的文件级
55
+ * 包装模块对称)。
56
+ *
57
+ * 尾标同样不能省:ID 若以 `.vue` 结尾,`@vitejs/plugin-vue` 会把它当真实 SFC 读盘
58
+ * (见 `WRAPPER_VIRTUAL_TAIL` 的说明)。
59
+ */
60
+ const SERVER_COMPONENT_STUB_PREFIX = "\0virtual:ubean-server-component-stub:";
49
61
  /** `.client.vue` 在 SSR 构建中的通用占位符虚拟模块 ID */
50
62
  const CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID = "virtual:ubean-client-component-placeholder";
51
63
  const CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID = `\0${CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID}`;
@@ -103,6 +115,104 @@ function wrapServerComponentTemplate(code) {
103
115
  return `${code.slice(0, tpl.start)}<template${tpl.attrs}>${newContent}</template>${code.slice(tpl.end)}`;
104
116
  }
105
117
  /**
118
+ * `.server.vue` 是否带 `<template>` 块。
119
+ *
120
+ * 用于把「包裹不了」的两种情况区分开:**没有 `<template>`**(render 函数 / `export { default }
121
+ * from …` 这类写法)必须报错,而**已经包裹过**(幂等)应当静默跳过。
122
+ *
123
+ * 为什么没有 `<template>` 就必须报错:客户端侧一律替换成 stub(渲染 `<ubean-server-only>`),
124
+ * 服务端侧的内容必须被包进**同名元素**才能在水合时对齐。没有 `<template>` 时服务端输出的是
125
+ * 组件自己的根元素,与 stub 的根标签不一致 —— 实测 Vue 报
126
+ * `Hydration node mismatch: rendered on server: JSHandle@node / expected on client: ubean-server-only`,
127
+ * 并且**把服务端渲染出来的内容清掉**(首屏有内容、水合后变空),比不渲染更糟。
128
+ */
129
+ function hasServerComponentTemplate(code) {
130
+ return extractTemplateBlock(code) !== null;
131
+ }
132
+ /**
133
+ * 父级元素只允许特定子元素、且浏览器解析器会**重构 DOM** 的那些标签。
134
+ *
135
+ * `.server.vue` 在客户端图里会渲染一个 `<ubean-server-only>` 包裹元素(客户端记录
136
+ * `vdom` 里的节点,水合才能保住服务端内容),而普通元素不可能在所有上下文都合法:
137
+ *
138
+ * - **表格上下文**:包裹元素被 foster-parent 到表格**之外**(实测:内容出现在表格前、
139
+ * `<tr>` 变空),随后水合报 `Hydration node mismatch` / `Hydration children mismatch`
140
+ * 并移除内容;生产模式下这些告警被剥掉,表现为静默的错位 + 内容丢失。
141
+ * - **`select` / `optgroup`**:解析器处于 "in select" 插入模式,无法识别的标签被**直接丢弃**。
142
+ *
143
+ * 刻意**不含** `ul` / `ol` / `dl` / `menu`:那些上下文里非 `li` 子元素虽然是不合法 HTML,但解析器
144
+ * 不会搬移或丢弃它(两侧 DOM 一致,水合正常),报错会是误报。
145
+ */
146
+ const RESTRICTED_SERVER_COMPONENT_PARENTS = /* @__PURE__ */ new Set([
147
+ "table",
148
+ "thead",
149
+ "tbody",
150
+ "tfoot",
151
+ "tr",
152
+ "colgroup",
153
+ "select",
154
+ "optgroup"
155
+ ]);
156
+ /** 对 DOM 嵌套透明、不改变实际父级的标签(判断嵌套时向上穿透)。 */
157
+ const TRANSPARENT_NESTING_TAGS = /* @__PURE__ */ new Set(["template", "slot"]);
158
+ /**
159
+ * 找出「直接作为受限父级子元素」使用的 `.server.vue` 组件(见
160
+ * {@link RESTRICTED_SERVER_COMPONENT_PARENTS})。
161
+ *
162
+ * 判据只看**静态可解析**的部分:标签名能在 `<script setup>` 的 import 映射里找到,且 import 路径
163
+ * 以 `.server.vue` 结尾。驼峰/连字符两种写法都认(`<ServerGreeting />` 与 `<server-greeting />`)。
164
+ * 通过 barrel 再导出、或运行时动态解析的组件看不出来 —— 宁可漏报也不误报。
165
+ */
166
+ function findRestrictedServerComponentUsage(code, importMap) {
167
+ const template = extractTemplateBlock(code)?.content ?? "";
168
+ if (!template) return [];
169
+ const usages = [];
170
+ const stack = [];
171
+ const tagRe = /<(\/?)([A-Za-z][\w.:-]*)((?:"[^"]*"|'[^']*'|[^>"'])*?)(\/?)>/g;
172
+ let match;
173
+ while ((match = tagRe.exec(template)) !== null) {
174
+ const [, closing, rawName, , selfClosing] = match;
175
+ const name = rawName;
176
+ if (closing) {
177
+ const idx = stack.lastIndexOf(name);
178
+ if (idx !== -1) stack.length = idx;
179
+ continue;
180
+ }
181
+ let parent;
182
+ for (let i = stack.length - 1; i >= 0; i--) if (!TRANSPARENT_NESTING_TAGS.has(stack[i].toLowerCase())) {
183
+ parent = stack[i];
184
+ break;
185
+ }
186
+ if (parent && RESTRICTED_SERVER_COMPONENT_PARENTS.has(parent.toLowerCase())) {
187
+ if ((importMap.get(name) ?? importMap.get(kebabToPascal(name)))?.endsWith(".server.vue")) usages.push({
188
+ parent: parent.toLowerCase(),
189
+ tag: name
190
+ });
191
+ }
192
+ if (!selfClosing && !VOID_TAGS.has(name.toLowerCase())) stack.push(name);
193
+ }
194
+ return usages;
195
+ }
196
+ /** 空元素(无闭合标签)——误当作父级会让后面的标签判错层级。 */
197
+ const VOID_TAGS = /* @__PURE__ */ new Set([
198
+ "area",
199
+ "base",
200
+ "br",
201
+ "col",
202
+ "embed",
203
+ "hr",
204
+ "img",
205
+ "input",
206
+ "link",
207
+ "meta",
208
+ "source",
209
+ "track",
210
+ "wbr"
211
+ ]);
212
+ function kebabToPascal(name) {
213
+ return name.split("-").map((part) => part ? part[0].toUpperCase() + part.slice(1) : part).join("");
214
+ }
215
+ /**
106
216
  * 判断 id 是否为 SFC 主模块(非 `?vue&type=...` 子查询)。
107
217
  *
108
218
  * 收集 island 组件时需要同时访问 `<script setup>` 与 `<template>` 块,
@@ -936,28 +1046,33 @@ function ubeanIslandsPlugin(_options = {}) {
936
1046
  async resolveId(id, importer, options) {
937
1047
  if (id === "virtual:ubean-islands-registry") return ISLANDS_REGISTRY_RESOLVED_ID;
938
1048
  if (!enabled) return void 0;
939
- if (!options?.ssr && !isVueSubRequest(id) && isServerComponentFile(id)) return SERVER_COMPONENT_STUB_RESOLVED_ID;
1049
+ if (!options?.ssr && !isVueSubRequest(id) && isServerComponentFile(id)) {
1050
+ const resolved = await this.resolve(id, importer, { skipSelf: true });
1051
+ return `${SERVER_COMPONENT_STUB_PREFIX}${resolved?.id ?? id}${WRAPPER_VIRTUAL_TAIL}`;
1052
+ }
940
1053
  if (!isVueSubRequest(id) && isClientComponentFile(id) && !importer?.startsWith(CLIENT_COMPONENT_WRAPPER_PREFIX) && !importer?.startsWith(PAIRED_COMPONENT_WRAPPER_PREFIX)) {
941
1054
  if (options?.ssr) return CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID;
942
1055
  const resolved = await this.resolve(id, importer, { skipSelf: true });
943
1056
  if (!resolved) return void 0;
944
1057
  return `${CLIENT_COMPONENT_WRAPPER_PREFIX}${resolved.id}${WRAPPER_VIRTUAL_TAIL}`;
945
1058
  }
946
- if (importer && !importer.startsWith("\0") && !isVueSubRequest(id) && id.endsWith(".vue") && !isServerComponentFile(id) && !isClientComponentFile(id) && id.startsWith(".")) {
947
- const importerDir = dirname(resolve(importer));
948
- const baseName = resolve(importerDir, id).slice(0, -4);
949
- const serverSibling = `${baseName}.server.vue`;
950
- const clientSibling = `${baseName}.client.vue`;
951
- const hasServer = existsSync(serverSibling);
952
- const hasClient = existsSync(clientSibling);
953
- if (hasServer && hasClient) return `${PAIRED_COMPONENT_WRAPPER_PREFIX}${serverSibling}${PAIRED_PATH_SEPARATOR}${clientSibling}${WRAPPER_VIRTUAL_TAIL}`;
954
- if (hasServer) return serverSibling;
955
- if (hasClient) return clientSibling;
1059
+ if (importer && !importer.startsWith("\0") && !isVueSubRequest(id) && id.endsWith(".vue") && !isServerComponentFile(id) && !isClientComponentFile(id)) {
1060
+ if (await this.resolve(id, importer, { skipSelf: true })) return void 0;
1061
+ const base = id.slice(0, -4);
1062
+ const serverSibling = await this.resolve(`${base}.server.vue`, importer, { skipSelf: true });
1063
+ const clientSibling = await this.resolve(`${base}.client.vue`, importer, { skipSelf: true });
1064
+ if (serverSibling && clientSibling) return `${PAIRED_COMPONENT_WRAPPER_PREFIX}${serverSibling.id}${PAIRED_PATH_SEPARATOR}${clientSibling.id}${WRAPPER_VIRTUAL_TAIL}`;
1065
+ if (serverSibling) return serverSibling.id;
1066
+ if (clientSibling) return clientSibling.id;
956
1067
  }
957
1068
  },
958
1069
  load(id, options) {
959
1070
  if (id === ISLANDS_REGISTRY_RESOLVED_ID) return generateRegistryModule(islandComponents);
960
1071
  if (id === SERVER_COMPONENT_STUB_RESOLVED_ID) return `import { ServerComponentStub } from '@ubean/islands/runtime';\nexport default ServerComponentStub;`;
1072
+ if (id.startsWith(SERVER_COMPONENT_STUB_PREFIX)) {
1073
+ const name = id.slice(37).replace(WRAPPER_VIRTUAL_TAIL, "").split("/").pop().replace(/\.server\.vue$/, "");
1074
+ return `import { ServerComponentStub } from '@ubean/islands/runtime';\nexport default { ...ServerComponentStub, name: ${JSON.stringify(name)} };\n`;
1075
+ }
961
1076
  if (id === CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID) return `import { ClientComponentPlaceholder } from '@ubean/islands/runtime';\nexport default ClientComponentPlaceholder;`;
962
1077
  if (id.startsWith(CLIENT_COMPONENT_WRAPPER_PREFIX)) {
963
1078
  const realPath = id.slice(32).replace(WRAPPER_VIRTUAL_TAIL, "");
@@ -990,6 +1105,18 @@ function ubeanIslandsPlugin(_options = {}) {
990
1105
  if (wrapped !== null) {
991
1106
  code = wrapped;
992
1107
  serverWrapped = true;
1108
+ } else if (!hasServerComponentTemplate(code)) throw new Error(`[ubean] \`${normalizedId}\` 是 \`.server.vue\`(Server Component),但没有 <template> 块。\n客户端侧会被替换成通用 stub,服务端内容必须包进 <ubean-server-only> 才能在水合时对齐;没有 <template> 时(render 函数、\`export { default } from …\` 等写法)两侧根元素不一致,水合会清掉服务端渲染的内容。
1109
+ 修法:改成 <template> 形式的 SFC。`);
1110
+ }
1111
+ if (code.includes(".server.vue")) {
1112
+ const restricted = findRestrictedServerComponentUsage(code, parseScriptImports(extractScriptBlock(code)));
1113
+ if (restricted.length > 0) {
1114
+ const detail = restricted.map((u) => `\`<${u.parent}> > <${u.tag} />\``).join("、");
1115
+ throw new Error(`[ubean] \`${normalizedId}\` 在受限父级里直接使用了 Server Component:${detail}。\n\`.server.vue\` 在客户端图里会渲染一个 <ubean-server-only> 包裹元素(客户端 vdom 必须有对应节点,水合才能保住服务端内容),而普通元素不可能在所有上下文都合法:
1116
+ · 表格上下文(table/tr/tbody/…):包裹元素被解析器提到表格**之外** —— 实测内容出现在表格前、\`<tr>\` 变空,随后水合报 mismatch 并**移除内容**(生产模式静默);
1117
+ · select/optgroup:解析器直接**丢弃**不认识的标签。
1118
+ 修法:让服务端组件自己渲染容器(如整个 <table>),或把它放进允许的子元素(<td> / <li>)。
1119
+ (\`.client.vue\` 没有这个限制:它的占位符是注释节点。)`);
993
1120
  }
994
1121
  }
995
1122
  if (!ANY_DIRECTIVE_RE.test(code) && !serverWrapped) return code !== originalCode ? { code } : null;
@@ -1017,4 +1144,4 @@ function ubeanIslandsPlugin(_options = {}) {
1017
1144
  };
1018
1145
  }
1019
1146
  //#endregion
1020
- export { CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID, ISLANDS_REGISTRY_VIRTUAL_ID, SERVER_COMPONENT_STUB_VIRTUAL_ID, collectIslandComponents, generateRegistryModule, injectServerComponentPath, isClientComponentFile, isServerComponentFile, parseScriptImports, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin, wrapServerComponentTemplate };
1147
+ export { CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID, ISLANDS_REGISTRY_VIRTUAL_ID, SERVER_COMPONENT_STUB_VIRTUAL_ID, collectIslandComponents, findRestrictedServerComponentUsage, generateRegistryModule, hasServerComponentTemplate, injectServerComponentPath, isClientComponentFile, isServerComponentFile, parseScriptImports, resolveIslandImportPath, scanIslandDirectiveNames, transformVueSfcIslands, ubeanIslandsPlugin, wrapServerComponentTemplate };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ubean/islands",
3
- "version": "0.6.0-beta.1",
3
+ "version": "0.6.0-beta.3",
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,18 @@
36
36
  }
37
37
  },
38
38
  "dependencies": {
39
- "@ubean/shared": "0.6.0-beta.1",
40
- "hono": "4.13.7",
41
- "vue": "^3.5.42"
39
+ "@ubean/shared": "0.6.0-beta.3",
40
+ "hono": "4.13.9",
41
+ "vue": "^3.5.43"
42
42
  },
43
43
  "devDependencies": {
44
- "@types/node": "^26.5.0",
44
+ "@types/node": "^26.6.2",
45
45
  "typescript": "npm:typescript-native-bridge@latest",
46
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1",
47
- "vite-plus": "0.3.1"
46
+ "vite": "npm:@voidzero-dev/vite-plus-core@latest",
47
+ "vite-plus": "latest"
48
48
  },
49
49
  "peerDependencies": {
50
- "vite": "npm:@voidzero-dev/vite-plus-core@0.3.1"
50
+ "vite": "npm:@voidzero-dev/vite-plus-core@latest"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "vite": {