@ubean/islands 0.5.1 → 0.6.0-beta.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/dist/vite.js +81 -10
- package/package.json +2 -2
package/dist/vite.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { legacyDirectiveToStrategy, strategyToLegacyDirective } from "./directive.js";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
//#region src/vite.ts
|
|
5
5
|
/**
|
|
6
6
|
* `v-client.*` 指令属性名(Phase 4: 唯一支持的客户端 island 语法)。
|
|
@@ -49,8 +49,27 @@ const SERVER_COMPONENT_STUB_RESOLVED_ID = `\0${SERVER_COMPONENT_STUB_VIRTUAL_ID}
|
|
|
49
49
|
/** `.client.vue` 在 SSR 构建中的通用占位符虚拟模块 ID */
|
|
50
50
|
const CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID = "virtual:ubean-client-component-placeholder";
|
|
51
51
|
const CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID = `\0${CLIENT_COMPONENT_PLACEHOLDER_VIRTUAL_ID}`;
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* `.client.vue` 在 client 构建中文件级包装虚拟模块 ID 前缀。
|
|
54
|
+
*
|
|
55
|
+
* ID 形如 `\0virtual:ubean-client-component:<真实路径><WRAPPER_TAIL>` —— **尾标不能省**:
|
|
56
|
+
* 若 ID 直接以 `.vue` 结尾,`@vitejs/plugin-vue` 会把它当作真实 SFC 去读盘,报
|
|
57
|
+
* `The argument 'path' must be a string … without null bytes`(实测:dev 走不到这条路径,
|
|
58
|
+
* 生产构建必挂)。尾标让文件名不再匹配 `/`\.vue$/`,Vue 插件自然跳过。
|
|
59
|
+
*/
|
|
53
60
|
const CLIENT_COMPONENT_WRAPPER_PREFIX = "\0virtual:ubean-client-component:";
|
|
61
|
+
/** 包装虚拟模块 ID 的尾标(见 `CLIENT_COMPONENT_WRAPPER_PREFIX` 的说明)。 */
|
|
62
|
+
const WRAPPER_VIRTUAL_TAIL = ".ubean-wrapper";
|
|
63
|
+
/**
|
|
64
|
+
* 是否是 `@vitejs/plugin-vue` 为 SFC 生成的**子请求**(`Foo.vue?vue&type=script&lang.ts` 等)。
|
|
65
|
+
*
|
|
66
|
+
* 包装/重定向**只对主文件 id 生效**:子请求是 Vue 插件自己的内部模块,重定向它们会得到
|
|
67
|
+
* `\0virtual:…:<真实路径>?vue&type=script…` 这种拼接产物 —— Vue 插件随后把它当真实文件读盘,
|
|
68
|
+
* 报 `The argument 'path' must be a string … without null bytes`(实测生产构建必挂、dev 侥幸不挂)。
|
|
69
|
+
*/
|
|
70
|
+
function isVueSubRequest(id) {
|
|
71
|
+
return id.includes("?");
|
|
72
|
+
}
|
|
54
73
|
/**
|
|
55
74
|
* Task 9.3: 配对组件 (`Foo.vue` 同时存在 `.server.vue` + `.client.vue`) 在
|
|
56
75
|
* SSR / client 构建中生成的虚拟包装模块 ID 前缀。
|
|
@@ -809,6 +828,33 @@ function transformVueSfcIslands(code, filePath) {
|
|
|
809
828
|
const ISLANDS_REGISTRY_VIRTUAL_ID = "virtual:ubean-islands-registry";
|
|
810
829
|
/** Vite 内部解析后的虚拟模块 ID(`\0` 前缀防止其他插件处理) */
|
|
811
830
|
const ISLANDS_REGISTRY_RESOLVED_ID = `\0${ISLANDS_REGISTRY_VIRTUAL_ID}`;
|
|
831
|
+
/** 扫描时要跳过的目录(构建产物与依赖不可能是用户写的 island 用法)。 */
|
|
832
|
+
const SCAN_SKIP_DIRS = [
|
|
833
|
+
"node_modules",
|
|
834
|
+
".git",
|
|
835
|
+
".ubean",
|
|
836
|
+
"dist",
|
|
837
|
+
".temp",
|
|
838
|
+
"coverage"
|
|
839
|
+
];
|
|
840
|
+
/** 递归列出项目内所有 `.vue` 源文件(绝对路径)。 */
|
|
841
|
+
function listVueFiles(root) {
|
|
842
|
+
const out = [];
|
|
843
|
+
const walk = (dir) => {
|
|
844
|
+
let entries;
|
|
845
|
+
try {
|
|
846
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
847
|
+
} catch {
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
for (const entry of entries) if (entry.isDirectory()) {
|
|
851
|
+
if (SCAN_SKIP_DIRS.includes(entry.name)) continue;
|
|
852
|
+
walk(join(dir, entry.name));
|
|
853
|
+
} else if (entry.isFile() && entry.name.endsWith(".vue")) out.push(join(dir, entry.name));
|
|
854
|
+
};
|
|
855
|
+
walk(root);
|
|
856
|
+
return out;
|
|
857
|
+
}
|
|
812
858
|
function ubeanIslandsPlugin(_options = {}) {
|
|
813
859
|
let viteConfig;
|
|
814
860
|
let enabled = true;
|
|
@@ -859,27 +905,52 @@ function ubeanIslandsPlugin(_options = {}) {
|
|
|
859
905
|
viteConfig = config;
|
|
860
906
|
enabled = _options.enabled !== false;
|
|
861
907
|
},
|
|
908
|
+
/**
|
|
909
|
+
* 构建期**主动**扫盘预填注册表(RM-V24)。
|
|
910
|
+
*
|
|
911
|
+
* 原先注册表只由 `transform` 在遍历 SFC 主模块时填充,而注册表模块在客户端构建里先于
|
|
912
|
+
* 页面被加载(页面是惰性 `() => import(...)`)—— 于是 `load` 读到的 map 为空,产出
|
|
913
|
+
* `export const islands = {}`,**岛屿组件永不进入 bundle**:页面只剩按名字渲染的
|
|
914
|
+
* `<ubean-island>` 外壳,水合时按注册表查不到组件。实测症状是产物里 `IslandClock` /
|
|
915
|
+
* `IslandCounter` 等 chunk 全部消失,而体积门禁只守增长,反而报「预算 OK」。
|
|
916
|
+
*
|
|
917
|
+
* 因此这里在构建开始前把磁盘上所有含 `v-client.*` 的 SFC 扫一遍预填 —— 注册表内容不再
|
|
918
|
+
* 依赖模块转换顺序。dev 路径行为不变(transform 仍会增量更新,HMR 清理逻辑照旧)。
|
|
919
|
+
*/
|
|
920
|
+
async buildStart() {
|
|
921
|
+
if (!enabled) return;
|
|
922
|
+
for (const file of listVueFiles(viteConfig.root)) {
|
|
923
|
+
let code;
|
|
924
|
+
try {
|
|
925
|
+
code = readFileSync(file, "utf-8");
|
|
926
|
+
} catch {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
if (!ANY_DIRECTIVE_RE.test(code)) continue;
|
|
930
|
+
updateRegistry(collectIslandComponents(code, file), file);
|
|
931
|
+
}
|
|
932
|
+
},
|
|
862
933
|
configureServer(server) {
|
|
863
934
|
devServer = server;
|
|
864
935
|
},
|
|
865
936
|
async resolveId(id, importer, options) {
|
|
866
937
|
if (id === "virtual:ubean-islands-registry") return ISLANDS_REGISTRY_RESOLVED_ID;
|
|
867
938
|
if (!enabled) return void 0;
|
|
868
|
-
if (!options?.ssr && isServerComponentFile(id)) return SERVER_COMPONENT_STUB_RESOLVED_ID;
|
|
869
|
-
if (isClientComponentFile(id) && !importer?.startsWith(CLIENT_COMPONENT_WRAPPER_PREFIX) && !importer?.startsWith(PAIRED_COMPONENT_WRAPPER_PREFIX)) {
|
|
939
|
+
if (!options?.ssr && !isVueSubRequest(id) && isServerComponentFile(id)) return SERVER_COMPONENT_STUB_RESOLVED_ID;
|
|
940
|
+
if (!isVueSubRequest(id) && isClientComponentFile(id) && !importer?.startsWith(CLIENT_COMPONENT_WRAPPER_PREFIX) && !importer?.startsWith(PAIRED_COMPONENT_WRAPPER_PREFIX)) {
|
|
870
941
|
if (options?.ssr) return CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID;
|
|
871
942
|
const resolved = await this.resolve(id, importer, { skipSelf: true });
|
|
872
943
|
if (!resolved) return void 0;
|
|
873
|
-
return `${CLIENT_COMPONENT_WRAPPER_PREFIX}${resolved.id}`;
|
|
944
|
+
return `${CLIENT_COMPONENT_WRAPPER_PREFIX}${resolved.id}${WRAPPER_VIRTUAL_TAIL}`;
|
|
874
945
|
}
|
|
875
|
-
if (importer && !importer.startsWith("\0") && id.endsWith(".vue") && !isServerComponentFile(id) && !isClientComponentFile(id) && id.startsWith(".")) {
|
|
946
|
+
if (importer && !importer.startsWith("\0") && !isVueSubRequest(id) && id.endsWith(".vue") && !isServerComponentFile(id) && !isClientComponentFile(id) && id.startsWith(".")) {
|
|
876
947
|
const importerDir = dirname(resolve(importer));
|
|
877
948
|
const baseName = resolve(importerDir, id).slice(0, -4);
|
|
878
949
|
const serverSibling = `${baseName}.server.vue`;
|
|
879
950
|
const clientSibling = `${baseName}.client.vue`;
|
|
880
951
|
const hasServer = existsSync(serverSibling);
|
|
881
952
|
const hasClient = existsSync(clientSibling);
|
|
882
|
-
if (hasServer && hasClient) return `${PAIRED_COMPONENT_WRAPPER_PREFIX}${serverSibling}${PAIRED_PATH_SEPARATOR}${clientSibling}`;
|
|
953
|
+
if (hasServer && hasClient) return `${PAIRED_COMPONENT_WRAPPER_PREFIX}${serverSibling}${PAIRED_PATH_SEPARATOR}${clientSibling}${WRAPPER_VIRTUAL_TAIL}`;
|
|
883
954
|
if (hasServer) return serverSibling;
|
|
884
955
|
if (hasClient) return clientSibling;
|
|
885
956
|
}
|
|
@@ -889,11 +960,11 @@ function ubeanIslandsPlugin(_options = {}) {
|
|
|
889
960
|
if (id === SERVER_COMPONENT_STUB_RESOLVED_ID) return `import { ServerComponentStub } from '@ubean/islands/runtime';\nexport default ServerComponentStub;`;
|
|
890
961
|
if (id === CLIENT_COMPONENT_PLACEHOLDER_RESOLVED_ID) return `import { ClientComponentPlaceholder } from '@ubean/islands/runtime';\nexport default ClientComponentPlaceholder;`;
|
|
891
962
|
if (id.startsWith(CLIENT_COMPONENT_WRAPPER_PREFIX)) {
|
|
892
|
-
const realPath = id.slice(32);
|
|
963
|
+
const realPath = id.slice(32).replace(WRAPPER_VIRTUAL_TAIL, "");
|
|
893
964
|
return `import RealComp from ${JSON.stringify(realPath)};\nimport { defineClientComponent } from '@ubean/islands/runtime';\nexport default defineClientComponent(RealComp);`;
|
|
894
965
|
}
|
|
895
966
|
if (id.startsWith(PAIRED_COMPONENT_WRAPPER_PREFIX)) {
|
|
896
|
-
const payload = id.slice(32);
|
|
967
|
+
const payload = id.slice(32).replace(WRAPPER_VIRTUAL_TAIL, "");
|
|
897
968
|
const sepIdx = payload.indexOf(PAIRED_PATH_SEPARATOR);
|
|
898
969
|
if (sepIdx === -1) return void 0;
|
|
899
970
|
const serverPath = payload.slice(0, sepIdx);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ubean/islands",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-beta.1",
|
|
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,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@ubean/shared": "0.
|
|
39
|
+
"@ubean/shared": "0.6.0-beta.1",
|
|
40
40
|
"hono": "4.13.7",
|
|
41
41
|
"vue": "^3.5.42"
|
|
42
42
|
},
|