@@ -61,9 +61,30 @@ const SERVER_COMPONENT_STUB_RESOLVED_ID = `\0${SERVER_COMPONENT_STUB_VIRTUAL_ID}
6161export const CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID = 'virtual:ubean-client-component-placeholder' ;
6262const CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID = `\0${ CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID } ` ;
6363
64- /** `.client.vue` 在 client 构建中文件级包装虚拟模块 ID 前缀 */
64+ /**
65+ * `.client.vue` 在 client 构建中文件级包装虚拟模块 ID 前缀。
66+ *
67+ * ID 形如 `\0virtual:ubean-client-component:<真实路径><WRAPPER_TAIL>` —— **尾标不能省**:
68+ * 若 ID 直接以 `.vue` 结尾,`@vitejs/plugin-vue` 会把它当作真实 SFC 去读盘,报
69+ * `The argument 'path' must be a string … without null bytes`(实测:dev 走不到这条路径,
70+ * 生产构建必挂)。尾标让文件名不再匹配 `/`\.vue$/`,Vue 插件自然跳过。
71+ */
6572const CLIENT_COMPONENT_WRAPPER_PREFIX = '\0virtual:ubean-client-component:' ;
6673
74+ /** 包装虚拟模块 ID 的尾标(见 `CLIENT_COMPONENT_WRAPPER_PREFIX` 的说明)。 */
75+ const WRAPPER_VIRTUAL_TAIL = '.ubean-wrapper' ;
76+
77+ /**
78+ * 是否是 `@vitejs/plugin-vue` 为 SFC 生成的**子请求**(`Foo.vue?vue&type=script&lang.ts` 等)。
79+ *
80+ * 包装/重定向**只对主文件 id 生效**:子请求是 Vue 插件自己的内部模块,重定向它们会得到
81+ * `\0virtual:…:<真实路径>?vue&type=script…` 这种拼接产物 —— Vue 插件随后把它当真实文件读盘,
82+ * 报 `The argument 'path' must be a string … without null bytes`(实测生产构建必挂、dev 侥幸不挂)。
83+ */
84+ function isVueSubRequest ( id : string ) : boolean {
85+ return id . includes ( '?' ) ;
86+ }
87+
6788/**
6889 * Task 9.3: 配对组件 (`Foo.vue` 同时存在 `.server.vue` + `.client.vue`) 在
6990 * SSR / client 构建中生成的虚拟包装模块 ID 前缀。
@@ -1141,7 +1162,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
11411162
11421163 // --- Task 9.1: .server.vue → client 构建重定向到通用 stub ---
11431164 // SSR 构建时正常解析到真实文件 (return undefined 走默认解析)
1144- if ( ! options ?. ssr && isServerComponentFile ( id ) ) {
1165+ if ( ! options ?. ssr && ! isVueSubRequest ( id ) && isServerComponentFile ( id ) ) {
11451166 return SERVER_COMPONENT_STUB_RESOLVED_ID ;
11461167 }
11471168
@@ -1151,6 +1172,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
11511172 // Task 9.3: 同样排除从配对组件包装模块内部的 import — 配对 wrapper 直接
11521173 // 导入真实 `.client.vue`,不需要 `defineClientComponent` 二次包装。
11531174 if (
1175+ ! isVueSubRequest ( id ) &&
11541176 isClientComponentFile ( id ) &&
11551177 ! importer ?. startsWith ( CLIENT_COMPONENT_WRAPPER_PREFIX ) &&
11561178 ! importer ?. startsWith ( PAIRED_COMPONENT_WRAPPER_PREFIX )
@@ -1162,7 +1184,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
11621184 // client: 解析真实路径,生成文件级包装虚拟模块
11631185 const resolved = await this . resolve ( id , importer , { skipSelf : true } ) ;
11641186 if ( ! resolved ) return undefined ;
1165- return `${ CLIENT_COMPONENT_WRAPPER_PREFIX } ${ resolved . id } ` ;
1187+ return `${ CLIENT_COMPONENT_WRAPPER_PREFIX } ${ resolved . id } ${ WRAPPER_VIRTUAL_TAIL } ` ;
11661188 }
11671189
11681190 // --- Task 9.3: 配对组件解析 — 普通 .vue 导入检查 .server.vue / .client.vue 兄弟文件 ---
@@ -1172,6 +1194,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
11721194 if (
11731195 importer &&
11741196 ! importer . startsWith ( '\0' ) &&
1197+ ! isVueSubRequest ( id ) &&
11751198 id . endsWith ( '.vue' ) &&
11761199 ! isServerComponentFile ( id ) &&
11771200 ! isClientComponentFile ( id ) &&
@@ -1188,7 +1211,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
11881211
11891212 if ( hasServer && hasClient ) {
11901213 // 配对:重定向到虚拟 wrapper 模块 (load 钩子根据 ssr 选项生成不同内容)
1191- return `${ PAIRED_COMPONENT_WRAPPER_PREFIX } ${ serverSibling } ${ PAIRED_PATH_SEPARATOR } ${ clientSibling } ` ;
1214+ return `${ PAIRED_COMPONENT_WRAPPER_PREFIX } ${ serverSibling } ${ PAIRED_PATH_SEPARATOR } ${ clientSibling } ${ WRAPPER_VIRTUAL_TAIL } ` ;
11921215 }
11931216 if ( hasServer ) {
11941217 // 仅存在 .server.vue:重定向 (SSR=真实文件, client=stub)
@@ -1222,7 +1245,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
12221245
12231246 // Task 9.2: .client.vue client wrapper — import 真实组件 + defineClientComponent
12241247 if ( id . startsWith ( CLIENT_COMPONENT_WRAPPER_PREFIX ) ) {
1225- const realPath = id . slice ( CLIENT_COMPONENT_WRAPPER_PREFIX . length ) ;
1248+ const realPath = id . slice ( CLIENT_COMPONENT_WRAPPER_PREFIX . length ) . replace ( WRAPPER_VIRTUAL_TAIL , '' ) ;
12261249 return (
12271250 `import RealComp from ${ JSON . stringify ( realPath ) } ;\n` +
12281251 `import { defineClientComponent } from '@ubean/islands/runtime';\n` +
@@ -1234,7 +1257,7 @@ export function ubeanIslandsPlugin(_options: UbeanIslandsPluginOptions = {}): Pl
12341257 // client 同时导入 .server.vue (→ stub) 与 .client.vue (→ 真实文件,通过 importer
12351258 // 检查跳过 defineClientComponent 包装),调用 definePairedComponent 切换。
12361259 if ( id . startsWith ( PAIRED_COMPONENT_WRAPPER_PREFIX ) ) {
1237- const payload = id . slice ( PAIRED_COMPONENT_WRAPPER_PREFIX . length ) ;
1260+ const payload = id . slice ( PAIRED_COMPONENT_WRAPPER_PREFIX . length ) . replace ( WRAPPER_VIRTUAL_TAIL , '' ) ;
12381261 const sepIdx = payload . indexOf ( PAIRED_PATH_SEPARATOR ) ;
12391262 if ( sepIdx === - 1 ) return undefined ;
12401263 const serverPath = payload . slice ( 0 , sepIdx ) ;
0 commit comments