@ubean/islands 0.5.2 → 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.
Files changed (2) hide show
  1. package/dist/vite.js +81 -10
  2. 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
- /** `.client.vue` 在 client 构建中文件级包装虚拟模块 ID 前缀 */
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.5.2",
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.5.2",
39
+ "@ubean/shared": "0.6.0-beta.1",
40
40
  "hono": "4.13.7",
41
41
  "vue": "^3.5.42"
42
42
  },