@pubinfo/vite 2.0.0-rc.2 → 2.0.0-rc.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/index.d.ts CHANGED
@@ -26,10 +26,6 @@ interface ModuleEntries {
26
26
  interface ResolverPluginOptions {
27
27
  /** 入口配置 */
28
28
  entries: ModuleEntries;
29
- /** 启用缓存优化 */
30
- cache?: boolean;
31
- /** 最大缓存大小 */
32
- maxCacheSize?: number;
33
29
  }
34
30
  //#endregion
35
31
  //#region src/interface.d.ts
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
- import { defineConfig, loadEnv, mergeConfig } from "rolldown-vite";
2
+ import { defineConfig, loadEnv, mergeConfig, normalizePath } from "rolldown-vite";
3
3
  import { cwd } from "node:process";
4
4
  import chalk from "chalk";
5
5
  import consola from "consola";
6
- import { join, posix, relative, resolve } from "node:path";
6
+ import { dirname, join, posix, relative, resolve } from "node:path";
7
7
  import vue from "@vitejs/plugin-vue";
8
8
  import vueJsx from "@vitejs/plugin-vue-jsx";
9
9
  import autoImport from "unplugin-auto-import/vite";
@@ -17,13 +17,15 @@ import boxen from "boxen";
17
17
  import { libInjectCss } from "vite-plugin-lib-inject-css";
18
18
  import VueDevTools from "vite-plugin-vue-devtools";
19
19
  import vueLegacy from "@vitejs/plugin-legacy";
20
- import { createHash } from "node:crypto";
21
- import { existsSync, statSync } from "node:fs";
22
20
  import fg from "fast-glob";
23
21
  import { merge } from "lodash-es";
22
+ import picomatch from "picomatch";
23
+ import { existsSync } from "node:fs";
24
24
  import { vitePluginFakeServer } from "vite-plugin-fake-server";
25
25
  import OpenAPI from "@pubinfo/unplugin-openapi/vite";
26
26
  import Unocss from "unocss/vite";
27
+ import { readFile } from "node:fs/promises";
28
+ import { fileURLToPath } from "node:url";
27
29
 
28
30
  export * from "rolldown-vite"
29
31
 
@@ -68,17 +70,20 @@ function getServerProxy(env, isProxy) {
68
70
  //#endregion
69
71
  //#region src/plugins/auto-import.ts
70
72
  function createAutoImport() {
71
- return autoImport({
72
- imports: [
73
- "vue",
74
- "vue-router",
75
- "pinia",
76
- { pubinfo: ["useAuth"] }
77
- ],
78
- ignore: ["h"],
79
- dts: "./.pubinfo/auto-imports.d.ts",
80
- resolvers: [AntDesignVueResolver(), IconsResolver({ prefix: "i" })]
81
- });
73
+ return {
74
+ ...autoImport({
75
+ imports: [
76
+ "vue",
77
+ "vue-router",
78
+ "pinia",
79
+ { pubinfo: ["useAuth"] }
80
+ ],
81
+ ignore: ["h"],
82
+ dts: "./.pubinfo/auto-imports.d.ts",
83
+ resolvers: [AntDesignVueResolver(), IconsResolver({ prefix: "i" })]
84
+ }),
85
+ enforce: "pre"
86
+ };
82
87
  }
83
88
 
84
89
  //#endregion
@@ -138,10 +143,13 @@ function createCompression(env) {
138
143
  //#endregion
139
144
  //#region src/plugins/dts.ts
140
145
  function createDTS() {
141
- return dts({
142
- clearPureImport: false,
143
- exclude: ["tests/**/*"]
144
- });
146
+ return {
147
+ ...dts({
148
+ clearPureImport: false,
149
+ exclude: ["tests/**/*"]
150
+ }),
151
+ enforce: "post"
152
+ };
145
153
  }
146
154
 
147
155
  //#endregion
@@ -227,32 +235,70 @@ function getPatternBase(pattern) {
227
235
  }
228
236
  return baseParts.join("/");
229
237
  }
230
- function normalizePath(path) {
238
+ function normalizePath$1(path) {
231
239
  return posix.normalize(path.replace(/\\/g, "/"));
232
240
  }
233
241
  function libResolverPlugin(options) {
234
242
  const virtualModuleId = "virtual:pubinfo-resolver";
235
243
  const resolvedId = `\0${virtualModuleId}`;
236
- const enableCache = options.cache ?? true;
237
- const maxCacheSize = options.maxCacheSize ?? 50;
238
- const cache = /* @__PURE__ */ new Map();
244
+ let serverRoot = "";
245
+ function sendUpdate(server, action, file) {
246
+ const payload = {
247
+ action,
248
+ file,
249
+ timestamp: Date.now()
250
+ };
251
+ server.ws.send({
252
+ type: "custom",
253
+ event: "pubinfo-resolver:update",
254
+ data: payload
255
+ });
256
+ }
257
+ function isMatchingFile(file) {
258
+ const normalizedFile = normalizePath$1(file);
259
+ const isVueOrTsFile = /\.(vue|tsx?|jsx?)$/.test(normalizedFile);
260
+ const relativePath = serverRoot ? normalizePath$1(relative(serverRoot, normalizedFile)) : normalizedFile;
261
+ const patterns = Object.values(options.entries);
262
+ const matchesPattern = patterns.some((pattern, index) => {
263
+ const matcher = picomatch(pattern);
264
+ const matches = matcher(relativePath);
265
+ return matches;
266
+ });
267
+ const result = matchesPattern && isVueOrTsFile;
268
+ return result;
269
+ }
239
270
  return {
240
271
  name: "vite-plugin-lib-resolver",
241
272
  enforce: "pre",
242
- buildStart() {
243
- if (!enableCache) cache.clear();
273
+ buildStart() {},
274
+ configureServer(server) {
275
+ serverRoot = server.config.root;
276
+ const encodedPath = "/@id/__x00__virtual:pubinfo-resolver";
277
+ server.middlewares.use((req, _res, next) => {
278
+ if (req.url && req.url.startsWith(encodedPath)) {
279
+ const mod = server.moduleGraph.getModuleById(resolvedId);
280
+ if (mod) server.moduleGraph.invalidateModule(mod);
281
+ }
282
+ next();
283
+ });
284
+ server.watcher.on("add", (file) => {
285
+ if (isMatchingFile(file)) sendUpdate(server, "add", file);
286
+ });
287
+ server.watcher.on("unlink", (file) => {
288
+ if (isMatchingFile(file)) sendUpdate(server, "unlink", file);
289
+ });
290
+ const patterns = Object.values(options.entries);
291
+ const bases = patterns.map((pattern) => {
292
+ const base = getPatternBase(pattern);
293
+ return resolve(server.config.root, base);
294
+ });
295
+ server.watcher.add([...patterns.map((p) => resolve(server.config.root, p)), ...bases]);
244
296
  },
245
297
  async resolveId(id) {
246
298
  return id === virtualModuleId ? resolvedId : null;
247
299
  },
248
300
  async load(id) {
249
301
  if (id !== resolvedId) return null;
250
- const configHash = createHash("md5").update(JSON.stringify(options.entries)).digest("hex");
251
- if (enableCache && cache.has(configHash)) {
252
- const cached = cache.get(configHash);
253
- const hasChanges = await checkForFileChanges(options.entries, cached.fileHashes);
254
- if (!hasChanges) return cached.content;
255
- }
256
302
  const moduleResults = await Promise.all(Object.entries(options.entries).map(async ([key, pattern]) => {
257
303
  try {
258
304
  const files = await fg(pattern, { absolute: true });
@@ -263,8 +309,8 @@ function libResolverPlugin(options) {
263
309
  };
264
310
  const base = getPatternBase(pattern);
265
311
  const imports = files.map((file) => {
266
- const absPath = normalizePath(file);
267
- const relToBase = normalizePath(relative(resolve(base), file));
312
+ const absPath = normalizePath$1(file);
313
+ const relToBase = normalizePath$1(relative(resolve(base), file));
268
314
  const relPath = posix.join(base, relToBase);
269
315
  return `"${relPath}": () => import("${absPath}")`;
270
316
  }).join(",\n");
@@ -284,57 +330,16 @@ function libResolverPlugin(options) {
284
330
  }));
285
331
  const moduleDefs = moduleResults.map((result) => result.content);
286
332
  const content = `export default {\n ${moduleDefs.join(",\n ")}\n}`;
287
- const fileHashes = /* @__PURE__ */ new Map();
288
- const allFiles = moduleResults.flatMap((result) => result.files);
289
- for (const file of allFiles) if (existsSync(file)) {
290
- const stats = statSync(file);
291
- const fileHash = createHash("md5").update(`${file}-${stats.mtimeMs}-${stats.size}`).digest("hex");
292
- fileHashes.set(file, fileHash);
293
- }
294
- if (enableCache) {
295
- if (cache.size >= maxCacheSize) {
296
- const oldestKey = cache.keys().next().value;
297
- if (oldestKey) cache.delete(oldestKey);
298
- }
299
- cache.set(configHash, {
300
- content,
301
- fileHashes,
302
- timestamp: Date.now()
303
- });
304
- }
305
333
  return content;
306
334
  }
307
335
  };
308
- async function checkForFileChanges(entries, cachedFileHashes) {
309
- try {
310
- for (const pattern of Object.values(entries)) {
311
- const files = await fg(pattern, { absolute: true });
312
- for (const file of files) {
313
- if (!existsSync(file)) return true;
314
- const stats = statSync(file);
315
- const currentFileHash = createHash("md5").update(`${file}-${stats.mtimeMs}-${stats.size}`).digest("hex");
316
- const cachedHash = cachedFileHashes.get(file);
317
- if (!cachedHash || cachedHash !== currentFileHash) return true;
318
- }
319
- for (const cachedFile of cachedFileHashes.keys()) if (!files.includes(cachedFile) && existsSync(cachedFile)) return true;
320
- }
321
- return false;
322
- } catch (error) {
323
- console.warn("[lib-resolver] Error checking file changes:", error);
324
- return true;
325
- }
326
- }
327
336
  }
328
337
  function createLibResolver(options) {
329
- return libResolverPlugin(merge({
330
- entries: {
331
- icons: "src/assets/icons/**/*",
332
- layouts: "src/layouts/*.vue",
333
- pages: "src/views/**/*.vue"
334
- },
335
- cache: true,
336
- maxCacheSize: 50
337
- }, options));
338
+ return libResolverPlugin(merge({ entries: {
339
+ icons: "src/assets/icons/**/*",
340
+ layouts: "src/layouts/*.vue",
341
+ pages: "src/views/**/*.vue"
342
+ } }, options));
338
343
  }
339
344
 
340
345
  //#endregion
@@ -362,6 +367,75 @@ function createUnocss() {
362
367
  return Unocss();
363
368
  }
364
369
 
370
+ //#endregion
371
+ //#region src/plugins/virtual-inspector.ts
372
+ function getInspectorPath() {
373
+ const pluginPath = normalizePath(dirname(fileURLToPath(import.meta.url)));
374
+ return pluginPath;
375
+ }
376
+ function createVirtualInspectorPlugin(options = {}) {
377
+ const { enabled = true } = options;
378
+ const inspectorPath = getInspectorPath();
379
+ const virtualModuleId = "virtual:vite-pubinfo-inspector:load.js";
380
+ let config;
381
+ return {
382
+ name: "vite-plugin-virtual-inspector",
383
+ enforce: "pre",
384
+ configureServer(server) {
385
+ if (!enabled) return;
386
+ server.middlewares.use("/__pubinfo_inspector__", (req, res, next) => {
387
+ if (req.url === "/info") {
388
+ res.setHeader("Content-Type", "application/json");
389
+ res.end(JSON.stringify({
390
+ virtualModule: virtualModuleId,
391
+ enabled: true,
392
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
393
+ }));
394
+ } else next();
395
+ });
396
+ },
397
+ async resolveId(importee) {
398
+ if (importee === virtualModuleId) {
399
+ const resolved = resolve(inspectorPath, "load.js");
400
+ return resolved;
401
+ }
402
+ return null;
403
+ },
404
+ async load(id) {
405
+ if (id.includes("load.js") && id.includes(inspectorPath)) {
406
+ if (!enabled) return "export default {};";
407
+ const filePath = resolve(inspectorPath, "load.js");
408
+ if (existsSync(filePath)) return await readFile(filePath, "utf-8");
409
+ else {
410
+ console.error(`Failed to find file for pubinfo-inspector: ${filePath}`);
411
+ return "export default { error: \"Failed to load inspector content\" };";
412
+ }
413
+ }
414
+ return null;
415
+ },
416
+ transformIndexHtml(html) {
417
+ if (!enabled) return;
418
+ return {
419
+ html,
420
+ tags: [{
421
+ tag: "script",
422
+ injectTo: "head",
423
+ attrs: {
424
+ type: "module",
425
+ src: `${config?.base || "/"}@id/${virtualModuleId}`
426
+ }
427
+ }]
428
+ };
429
+ },
430
+ configResolved(resolvedConfig) {
431
+ config = resolvedConfig;
432
+ }
433
+ };
434
+ }
435
+ function createVirtualInspector(options) {
436
+ return createVirtualInspectorPlugin(options);
437
+ }
438
+
365
439
  //#endregion
366
440
  //#region src/plugins/index.ts
367
441
  function createVitePlugins(viteEnv, isBuild = false, config, type) {
@@ -378,6 +452,7 @@ function createVitePlugins(viteEnv, isBuild = false, config, type) {
378
452
  createOpenAPI(config.openapi),
379
453
  createLibResolver(config.resolver),
380
454
  appInfo(),
455
+ createVirtualInspector({ enabled: true }),
381
456
  type === "module" ? [createDTS(), createInjectCSS()] : null,
382
457
  isBuild ? createCompression(viteEnv) : null
383
458
  ];
package/dist/load.js ADDED
@@ -0,0 +1,267 @@
1
+ // Vite Pubinfo Inspector Virtual Module
2
+
3
+ // 检查器工具函数
4
+ export const inspectorUtils = {
5
+ // 获取当前环境信息
6
+ getEnvInfo() {
7
+ return {
8
+ nodeEnv: 'development', // 简化处理,直接设置为开发模式
9
+ mode: import.meta.env.MODE,
10
+ dev: import.meta.env.DEV,
11
+ prod: import.meta.env.PROD,
12
+ ssr: import.meta.env.SSR,
13
+ };
14
+ },
15
+
16
+ // 获取Vite信息
17
+ getViteInfo() {
18
+ return {
19
+ hmr: import.meta.hot ? 'enabled' : 'disabled',
20
+ base: import.meta.env.BASE_URL,
21
+ url: import.meta.url,
22
+ };
23
+ },
24
+
25
+ // WebSocket 连接工具
26
+ websocket: {
27
+ // 获取 HMR WebSocket 连接信息
28
+ getHMRConnection() {
29
+ if (!import.meta.hot) {
30
+ return null;
31
+ }
32
+
33
+ // 通过 import.meta.hot 获取内部的 transport 和连接信息
34
+ // 注意:这些是内部 API,可能会在 Vite 版本更新时发生变化
35
+ const hotContext = import.meta.hot;
36
+
37
+ // 尝试访问内部的 HMR 客户端
38
+ const hmrClient = hotContext?.__viteHmrClient || hotContext?._hmrClient;
39
+
40
+ return {
41
+ isAvailable: !!hotContext,
42
+ client: hmrClient,
43
+ // 提供一些基本的连接信息
44
+ connectionState: hmrClient?.transport ? 'connected' : 'disconnected',
45
+ };
46
+ },
47
+
48
+ // 发送自定义 WebSocket 消息
49
+ send(event, data) {
50
+ if (!import.meta.hot) {
51
+ console.warn('[Pubinfo Inspector] HMR not available');
52
+ return false;
53
+ }
54
+
55
+ try {
56
+ import.meta.hot.send(event, data);
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ console.error('[Pubinfo Inspector] Failed to send WebSocket message:', error);
61
+ return false;
62
+ }
63
+ },
64
+
65
+ // 监听自定义 WebSocket 事件
66
+ on(event, callback) {
67
+ if (!import.meta.hot) {
68
+ console.warn('[Pubinfo Inspector] HMR not available');
69
+ return () => {};
70
+ }
71
+
72
+ try {
73
+ import.meta.hot.on(event, callback);
74
+
75
+ // 返回取消监听的函数
76
+ return () => {
77
+ try {
78
+ import.meta.hot.off(event, callback);
79
+ }
80
+ catch (error) {
81
+ console.error('[Pubinfo Inspector] Failed to remove WebSocket listener:', error);
82
+ }
83
+ };
84
+ }
85
+ catch (error) {
86
+ console.error('[Pubinfo Inspector] Failed to add WebSocket listener:', error);
87
+ return () => {};
88
+ }
89
+ },
90
+
91
+ // 监听 WebSocket 连接状态变化
92
+ onConnectionChange(callback) {
93
+ if (!import.meta.hot) {
94
+ console.warn('[Pubinfo Inspector] HMR not available');
95
+ return () => {};
96
+ }
97
+
98
+ const listeners = [];
99
+
100
+ // 监听连接建立
101
+ const offConnect = this.on('vite:ws:connect', () => {
102
+ callback('connected');
103
+ });
104
+ listeners.push(offConnect);
105
+
106
+ // 监听连接断开
107
+ const offDisconnect = this.on('vite:ws:disconnect', () => {
108
+ callback('disconnected');
109
+ });
110
+ listeners.push(offDisconnect);
111
+
112
+ // 返回取消所有监听的函数
113
+ return () => {
114
+ listeners.forEach(off => off());
115
+ };
116
+ },
117
+
118
+ // 获取 WebSocket 连接的详细信息
119
+ getConnectionDetails() {
120
+ const connection = this.getHMRConnection();
121
+
122
+ if (!connection || !connection.isAvailable) {
123
+ return null;
124
+ }
125
+
126
+ // 尝试获取更多连接详细信息
127
+ const details = {
128
+ state: connection.connectionState,
129
+ url: null,
130
+ protocol: null,
131
+ readyState: null,
132
+ };
133
+
134
+ try {
135
+ // 尝试从内部访问 WebSocket 实例
136
+ const transport = connection.client?.transport;
137
+ if (transport?.socket) {
138
+ details.url = transport.socket.url;
139
+ details.protocol = transport.socket.protocol;
140
+ details.readyState = transport.socket.readyState;
141
+ }
142
+ }
143
+ catch (error) {
144
+ // 静默处理错误,因为这些是内部 API
145
+ }
146
+
147
+ return details;
148
+ },
149
+
150
+ // 测试 WebSocket 连接
151
+ async testConnection() {
152
+ if (!import.meta.hot) {
153
+ return { success: false, error: 'HMR not available' };
154
+ }
155
+
156
+ try {
157
+ // 发送一个测试事件
158
+ const testEventName = `pubinfo:test:${Date.now()}`;
159
+ let received = false;
160
+
161
+ // 监听响应
162
+ const off = this.on(`${testEventName}:response`, () => {
163
+ received = true;
164
+ });
165
+
166
+ // 发送测试消息
167
+ this.send(testEventName, { timestamp: Date.now() });
168
+
169
+ // 等待响应(超时 5 秒)
170
+ await new Promise(resolve => setTimeout(resolve, 100));
171
+
172
+ off();
173
+
174
+ return {
175
+ success: true,
176
+ connectionWorking: received,
177
+ details: this.getConnectionDetails(),
178
+ };
179
+ }
180
+ catch (error) {
181
+ return {
182
+ success: false,
183
+ error: error.message,
184
+ };
185
+ }
186
+ },
187
+ },
188
+
189
+ // 性能监控
190
+ performance: {
191
+ mark: (name) => {
192
+ if (typeof performance !== 'undefined' && performance.mark) {
193
+ performance.mark(name);
194
+ }
195
+ },
196
+ measure: (name, startMark, endMark) => {
197
+ if (typeof performance !== 'undefined' && performance.measure) {
198
+ try {
199
+ performance.measure(name, startMark, endMark);
200
+ const entries = performance.getEntriesByName(name);
201
+ return entries[entries.length - 1];
202
+ }
203
+ catch (e) {
204
+ console.warn('Performance measurement failed:', e);
205
+ return null;
206
+ }
207
+ }
208
+ return null;
209
+ },
210
+ },
211
+ };
212
+
213
+ // 导出默认对象
214
+ export default inspectorUtils;
215
+
216
+ // 全局访问接口 - 让其他模块可以访问 WebSocket 功能
217
+ if (typeof globalThis !== 'undefined') {
218
+ // 在全局对象上暴露 WebSocket 工具
219
+ globalThis.__PUBINFO_WEBSOCKET__ = {
220
+ // 发送消息
221
+ send: (event, data) => inspectorUtils.websocket.send(event, data),
222
+
223
+ // 监听事件
224
+ on: (event, callback) => inspectorUtils.websocket.on(event, callback),
225
+
226
+ // 获取连接信息
227
+ getConnection: () => inspectorUtils.websocket.getHMRConnection(),
228
+
229
+ // 获取连接详情
230
+ getDetails: () => inspectorUtils.websocket.getConnectionDetails(),
231
+
232
+ // 测试连接
233
+ test: () => inspectorUtils.websocket.testConnection(),
234
+
235
+ // 监听连接状态
236
+ onStateChange: callback => inspectorUtils.websocket.onConnectionChange(callback),
237
+ };
238
+ }
239
+
240
+ // HMR 支持 - 静默接受热更新,避免页面重新加载
241
+ if (import.meta.hot) {
242
+ // 静默接受热更新,不输出日志,避免噪音
243
+ import.meta.hot.accept(() => {
244
+ // 静默重新初始化全局接口,不输出任何日志
245
+ if (typeof globalThis !== 'undefined') {
246
+ globalThis.__PUBINFO_WEBSOCKET__ = {
247
+ send: (event, data) => inspectorUtils.websocket.send(event, data),
248
+ on: (event, callback) => inspectorUtils.websocket.on(event, callback),
249
+ getConnection: () => inspectorUtils.websocket.getHMRConnection(),
250
+ getDetails: () => inspectorUtils.websocket.getConnectionDetails(),
251
+ test: () => inspectorUtils.websocket.testConnection(),
252
+ onStateChange: callback => inspectorUtils.websocket.onConnectionChange(callback),
253
+ };
254
+ }
255
+ });
256
+
257
+ // HMR 数据持久化
258
+ import.meta.hot.dispose((data) => {
259
+ // 静默保存当前状态
260
+ data.connectionInfo = inspectorUtils.websocket.getConnectionDetails();
261
+ });
262
+
263
+ // 静默从之前的状态恢复
264
+ if (import.meta.hot.data?.connectionInfo) {
265
+ // 静默恢复,不输出日志
266
+ }
267
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pubinfo/vite",
3
3
  "type": "module",
4
- "version": "2.0.0-rc.2",
4
+ "version": "2.0.0-rc.3",
5
5
  "exports": {
6
6
  ".": {
7
7
  "types": "./dist/index.d.ts",
@@ -19,18 +19,20 @@
19
19
  "vue-i18n": "^10.0.7"
20
20
  },
21
21
  "dependencies": {
22
- "@pubinfo/unplugin-openapi": "^0.8.4",
22
+ "@pubinfo/unplugin-openapi": "^0.9.0",
23
23
  "@vitejs/plugin-legacy": "^7.2.1",
24
24
  "@vitejs/plugin-vue": "^6.0.0",
25
25
  "@vitejs/plugin-vue-jsx": "^5.0.1",
26
26
  "abort-controller": "^3.0.0",
27
27
  "boxen": "^8.0.1",
28
28
  "chalk": "^5.4.1",
29
+ "chokidar": "^4.0.1",
29
30
  "consola": "^3.4.2",
30
31
  "fast-glob": "^3.3.3",
31
32
  "fs-extra": "^11.3.0",
32
33
  "jszip": "^3.10.1",
33
34
  "lodash-es": "^4.17.21",
35
+ "picomatch": "^4.0.3",
34
36
  "rolldown-vite": "^7.1.2",
35
37
  "terser": "^5.43.1",
36
38
  "unocss": "^66.4.2",
@@ -48,6 +50,7 @@
48
50
  "@types/fs-extra": "^11.0.4",
49
51
  "@types/lodash-es": "^4.17.12",
50
52
  "@types/node": "^24.0.10",
53
+ "@types/picomatch": "^4.0.2",
51
54
  "vue": "^3.5.17",
52
55
  "vue-i18n": "^10.0.7"
53
56
  },