@vue/typescript-plugin 2.1.10 → 2.2.0

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/lib/client.d.ts CHANGED
@@ -6,11 +6,12 @@ export declare function collectExtractProps(...args: Parameters<typeof import('.
6
6
  export declare function getImportPathForFile(...args: Parameters<typeof import('./requests/getImportPathForFile.js')['getImportPathForFile']>): Promise<string | null | undefined>;
7
7
  export declare function getPropertiesAtLocation(...args: Parameters<typeof import('./requests/getPropertiesAtLocation.js')['getPropertiesAtLocation']>): Promise<string[] | null | undefined>;
8
8
  export declare function getQuickInfoAtPosition(...args: Parameters<typeof import('./requests/getQuickInfoAtPosition.js')['getQuickInfoAtPosition']>): Promise<string | null | undefined>;
9
- export declare function getComponentProps(...args: Parameters<typeof import('./requests/componentInfos.js')['getComponentProps']>): Promise<{
9
+ export declare function getComponentProps(fileName: string, componentName: string): Promise<{
10
10
  name: string;
11
- commentMarkdown: string;
11
+ required?: true;
12
+ commentMarkdown?: string;
12
13
  }[] | null | undefined>;
13
14
  export declare function getComponentEvents(...args: Parameters<typeof import('./requests/componentInfos.js')['getComponentEvents']>): Promise<string[] | null | undefined>;
14
15
  export declare function getTemplateContextProps(...args: Parameters<typeof import('./requests/componentInfos.js')['getTemplateContextProps']>): Promise<string[] | null | undefined>;
15
- export declare function getComponentNames(...args: Parameters<typeof import('./requests/componentInfos.js')['getComponentNames']>): Promise<string[] | null | undefined>;
16
+ export declare function getComponentNames(fileName: string): Promise<string[] | undefined>;
16
17
  export declare function getElementAttrs(...args: Parameters<typeof import('./requests/componentInfos.js')['getElementAttrs']>): Promise<string[] | null | undefined>;
package/lib/client.js CHANGED
@@ -11,68 +11,50 @@ exports.getComponentNames = getComponentNames;
11
11
  exports.getElementAttrs = getElementAttrs;
12
12
  const utils_1 = require("./utils");
13
13
  function collectExtractProps(...args) {
14
- return sendRequest({
15
- type: 'collectExtractProps',
16
- args,
17
- });
14
+ return sendRequest('collectExtractProps', ...args);
18
15
  }
19
16
  async function getImportPathForFile(...args) {
20
- return await sendRequest({
21
- type: 'getImportPathForFile',
22
- args,
23
- });
17
+ return await sendRequest('getImportPathForFile', ...args);
24
18
  }
25
19
  async function getPropertiesAtLocation(...args) {
26
- return await sendRequest({
27
- type: 'getPropertiesAtLocation',
28
- args,
29
- });
20
+ return await sendRequest('getPropertiesAtLocation', ...args);
30
21
  }
31
22
  function getQuickInfoAtPosition(...args) {
32
- return sendRequest({
33
- type: 'getQuickInfoAtPosition',
34
- args,
35
- });
23
+ return sendRequest('getQuickInfoAtPosition', ...args);
36
24
  }
37
25
  // Component Infos
38
- function getComponentProps(...args) {
39
- return sendRequest({
40
- type: 'getComponentProps',
41
- args,
42
- });
26
+ async function getComponentProps(fileName, componentName) {
27
+ const server = await (0, utils_1.getBestServer)(fileName);
28
+ if (!server) {
29
+ return;
30
+ }
31
+ return await server.getComponentProps(fileName, componentName);
43
32
  }
44
33
  function getComponentEvents(...args) {
45
- return sendRequest({
46
- type: 'getComponentEvents',
47
- args,
48
- });
34
+ return sendRequest('getComponentEvents', ...args);
49
35
  }
50
36
  function getTemplateContextProps(...args) {
51
- return sendRequest({
52
- type: 'getTemplateContextProps',
53
- args,
54
- });
37
+ return sendRequest('getTemplateContextProps', ...args);
55
38
  }
56
- function getComponentNames(...args) {
57
- return sendRequest({
58
- type: 'getComponentNames',
59
- args,
60
- });
39
+ async function getComponentNames(fileName) {
40
+ const server = await (0, utils_1.getBestServer)(fileName);
41
+ if (!server) {
42
+ return;
43
+ }
44
+ const componentAndProps = server.componentNamesAndProps.get(fileName);
45
+ if (!componentAndProps) {
46
+ return;
47
+ }
48
+ return Object.keys(componentAndProps);
61
49
  }
62
50
  function getElementAttrs(...args) {
63
- return sendRequest({
64
- type: 'getElementAttrs',
65
- args,
66
- });
51
+ return sendRequest('getElementAttrs', ...args);
67
52
  }
68
- async function sendRequest(request) {
69
- const server = (await (0, utils_1.searchNamedPipeServerForFile)(request.args[0]));
53
+ async function sendRequest(requestType, fileName, ...rest) {
54
+ const server = await (0, utils_1.getBestServer)(fileName);
70
55
  if (!server) {
71
- console.warn('[Vue Named Pipe Client] No server found for', request.args[0]);
72
56
  return;
73
57
  }
74
- const res = await (0, utils_1.sendRequestWorker)(request, server.socket);
75
- server.socket.end();
76
- return res;
58
+ return server.sendRequest(requestType, fileName, ...rest);
77
59
  }
78
60
  //# sourceMappingURL=client.js.map
package/lib/common.js CHANGED
@@ -42,8 +42,8 @@ function getCompletionsAtPosition(vueOptions, getCompletionsAtPosition) {
42
42
  const result = getCompletionsAtPosition(fileName, position, options, formattingSettings);
43
43
  if (result) {
44
44
  // filter __VLS_
45
- result.entries = result.entries.filter(entry => entry.name.indexOf('__VLS_') === -1
46
- && (!entry.labelDetails?.description || entry.labelDetails.description.indexOf('__VLS_') === -1));
45
+ result.entries = result.entries.filter(entry => !entry.name.includes('__VLS_')
46
+ && !entry.labelDetails?.description?.includes('__VLS_'));
47
47
  // modify label
48
48
  for (const item of result.entries) {
49
49
  if (item.source) {
@@ -124,7 +124,7 @@ function getCodeFixesAtPosition(getCodeFixesAtPosition) {
124
124
  return (...args) => {
125
125
  let result = getCodeFixesAtPosition(...args);
126
126
  // filter __VLS_
127
- result = result.filter(entry => entry.description.indexOf('__VLS_') === -1);
127
+ result = result.filter(entry => !entry.description.includes('__VLS_'));
128
128
  return result;
129
129
  };
130
130
  }
@@ -170,11 +170,12 @@ function getEncodedSemanticClassifications(ts, language, languageService, asScri
170
170
  return (filePath, span, format) => {
171
171
  const fileName = filePath.replace(windowsPathReg, '/');
172
172
  const result = getEncodedSemanticClassifications(fileName, span, format);
173
- const file = language.scripts.get(asScriptId(fileName));
174
- if (file?.generated?.root instanceof language_core_1.VueVirtualCode) {
175
- const { template } = file.generated.root._sfc;
173
+ const sourceScript = language.scripts.get(asScriptId(fileName));
174
+ const root = sourceScript?.generated?.root;
175
+ if (root instanceof language_core_1.VueVirtualCode) {
176
+ const { template } = root._sfc;
176
177
  if (template) {
177
- for (const componentSpan of getComponentSpans.call({ typescript: ts, languageService }, file.generated.root, template, {
178
+ for (const componentSpan of getComponentSpans.call({ typescript: ts, languageService }, root, template, {
178
179
  start: span.start - template.startTagEnd,
179
180
  length: span.length,
180
181
  })) {
@@ -4,17 +4,21 @@ exports.collectExtractProps = collectExtractProps;
4
4
  const language_core_1 = require("@vue/language-core");
5
5
  function collectExtractProps(fileName, templateCodeRange) {
6
6
  const { typescript: ts, languageService, language, isTsPlugin, getFileId } = this;
7
- const volarFile = language.scripts.get(getFileId(fileName));
8
- if (!(volarFile?.generated?.root instanceof language_core_1.VueVirtualCode)) {
7
+ const sourceScript = language.scripts.get(getFileId(fileName));
8
+ if (!sourceScript?.generated) {
9
+ return;
10
+ }
11
+ const root = sourceScript.generated.root;
12
+ if (!(root instanceof language_core_1.VueVirtualCode)) {
9
13
  return;
10
14
  }
11
15
  const result = new Map();
12
16
  const program = languageService.getProgram();
13
17
  const sourceFile = program.getSourceFile(fileName);
14
18
  const checker = program.getTypeChecker();
15
- const script = volarFile.generated?.languagePlugin.typescript?.getServiceScript(volarFile.generated.root);
19
+ const script = sourceScript.generated?.languagePlugin.typescript?.getServiceScript(root);
16
20
  const maps = script ? [...language.maps.forEach(script.code)].map(([_sourceScript, map]) => map) : [];
17
- const sfc = volarFile.generated.root._sfc;
21
+ const sfc = root._sfc;
18
22
  sourceFile.forEachChild(function visit(node) {
19
23
  if (ts.isPropertyAccessExpression(node)
20
24
  && ts.isIdentifier(node.expression)
@@ -23,7 +27,7 @@ function collectExtractProps(fileName, templateCodeRange) {
23
27
  const { name } = node;
24
28
  for (const map of maps) {
25
29
  let mapped = false;
26
- for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? volarFile.snapshot.getLength() : 0))) {
30
+ for (const source of map.toSourceLocation(name.getEnd() - (isTsPlugin ? sourceScript.snapshot.getLength() : 0))) {
27
31
  if (source[0] >= sfc.template.startTagEnd + templateCodeRange[0]
28
32
  && source[0] <= sfc.template.startTagEnd + templateCodeRange[1]
29
33
  && (0, language_core_1.isSemanticTokensEnabled)(source[1].data)) {
@@ -1,9 +1,10 @@
1
1
  import * as vue from '@vue/language-core';
2
2
  import type * as ts from 'typescript';
3
3
  import type { RequestContext } from './types';
4
- export declare function getComponentProps(this: RequestContext, fileName: string, tag: string, requiredOnly?: boolean): {
4
+ export declare function getComponentProps(this: RequestContext, fileName: string, tag: string): {
5
5
  name: string;
6
- commentMarkdown: string;
6
+ required?: true;
7
+ commentMarkdown?: string;
7
8
  }[] | undefined;
8
9
  export declare function getComponentEvents(this: RequestContext, fileName: string, tag: string): string[] | undefined;
9
10
  export declare function getTemplateContextProps(this: RequestContext, fileName: string): string[] | undefined;
@@ -8,7 +8,7 @@ exports._getComponentNames = _getComponentNames;
8
8
  exports.getElementAttrs = getElementAttrs;
9
9
  const vue = require("@vue/language-core");
10
10
  const shared_1 = require("@vue/shared");
11
- function getComponentProps(fileName, tag, requiredOnly = false) {
11
+ function getComponentProps(fileName, tag) {
12
12
  const { typescript: ts, language, languageService, getFileId } = this;
13
13
  const volarFile = language.scripts.get(getFileId(fileName));
14
14
  if (!(volarFile?.generated?.root instanceof vue.VueVirtualCode)) {
@@ -22,11 +22,9 @@ function getComponentProps(fileName, tag, requiredOnly = false) {
22
22
  return [];
23
23
  }
24
24
  const name = tag.split('.');
25
- let componentSymbol = components.type.getProperty(name[0]);
26
- if (!componentSymbol) {
27
- componentSymbol = components.type.getProperty((0, shared_1.camelize)(name[0]))
28
- ?? components.type.getProperty((0, shared_1.capitalize)((0, shared_1.camelize)(name[0])));
29
- }
25
+ let componentSymbol = components.type.getProperty(name[0])
26
+ ?? components.type.getProperty((0, shared_1.camelize)(name[0]))
27
+ ?? components.type.getProperty((0, shared_1.capitalize)((0, shared_1.camelize)(name[0])));
30
28
  if (!componentSymbol) {
31
29
  return [];
32
30
  }
@@ -47,11 +45,10 @@ function getComponentProps(fileName, tag, requiredOnly = false) {
47
45
  const propsType = checker.getTypeOfSymbolAtLocation(propParam, components.node);
48
46
  const props = propsType.getProperties();
49
47
  for (const prop of props) {
50
- if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) {
51
- const name = prop.name;
52
- const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());
53
- result.set(name, { name, commentMarkdown });
54
- }
48
+ const name = prop.name;
49
+ const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
50
+ const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;
51
+ result.set(name, { name, required, commentMarkdown });
55
52
  }
56
53
  }
57
54
  }
@@ -65,11 +62,10 @@ function getComponentProps(fileName, tag, requiredOnly = false) {
65
62
  if (prop.flags & ts.SymbolFlags.Method) { // #2443
66
63
  continue;
67
64
  }
68
- if (!requiredOnly || !(prop.flags & ts.SymbolFlags.Optional)) {
69
- const name = prop.name;
70
- const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags());
71
- result.set(name, { name, commentMarkdown });
72
- }
65
+ const name = prop.name;
66
+ const required = !(prop.flags & ts.SymbolFlags.Optional) || undefined;
67
+ const commentMarkdown = generateCommentMarkdown(prop.getDocumentationComment(checker), prop.getJsDocTags()) || undefined;
68
+ result.set(name, { name, required, commentMarkdown });
73
69
  }
74
70
  }
75
71
  }
@@ -155,7 +151,7 @@ function getComponentNames(fileName) {
155
151
  ?.type
156
152
  ?.getProperties()
157
153
  .map(c => c.name)
158
- .filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_'))
154
+ .filter(entry => !entry.includes('$') && !entry.startsWith('_'))
159
155
  ?? [];
160
156
  }
161
157
  function _getComponentNames(ts, tsLs, vueCode) {
@@ -163,7 +159,7 @@ function _getComponentNames(ts, tsLs, vueCode) {
163
159
  ?.type
164
160
  ?.getProperties()
165
161
  .map(c => c.name)
166
- .filter(entry => entry.indexOf('$') === -1 && !entry.startsWith('_'))
162
+ .filter(entry => !entry.includes('$') && !entry.startsWith('_'))
167
163
  ?? [];
168
164
  }
169
165
  function getElementAttrs(fileName, tagName) {
@@ -1,7 +1,7 @@
1
1
  import type { Language } from '@vue/language-core';
2
2
  import type * as ts from 'typescript';
3
3
  export interface RequestContext<T = any> {
4
- typescript: typeof import('typescript');
4
+ typescript: typeof ts;
5
5
  languageService: ts.LanguageService;
6
6
  languageServiceHost: ts.LanguageServiceHost;
7
7
  language: Language<T>;
package/lib/server.d.ts CHANGED
@@ -1,9 +1,22 @@
1
- import type { Language } from '@vue/language-core';
1
+ import { Language } from '@vue/language-core';
2
2
  import type * as ts from 'typescript';
3
- export interface Request {
4
- type: 'containsFile' | 'projectInfo' | 'collectExtractProps' | 'getImportPathForFile' | 'getPropertiesAtLocation' | 'getQuickInfoAtPosition' | 'getComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' | 'getComponentNames' | 'getElementAttrs';
5
- args: [fileName: string, ...rest: any];
6
- }
3
+ export type RequestType = 'containsFile' | 'projectInfo' | 'collectExtractProps' | 'getImportPathForFile' | 'getPropertiesAtLocation' | 'getQuickInfoAtPosition' | 'subscribeComponentProps' | 'getComponentEvents' | 'getTemplateContextProps' | 'getElementAttrs';
4
+ export type NotificationType = 'componentNamesUpdated' | 'componentPropsUpdated';
5
+ export type RequestData = [
6
+ seq: number,
7
+ type: RequestType,
8
+ fileName: string,
9
+ ...args: any[]
10
+ ];
11
+ export type ResponseData = [
12
+ seq: number,
13
+ data: any
14
+ ];
15
+ export type NotificationData = [
16
+ type: NotificationType,
17
+ fileName: string,
18
+ data: any
19
+ ];
7
20
  export interface ProjectInfo {
8
21
  name: string;
9
22
  kind: ts.server.ProjectKind;
package/lib/server.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.startNamedPipeServer = startNamedPipeServer;
4
- const fs = require("fs");
5
- const net = require("net");
4
+ const language_core_1 = require("@vue/language-core");
5
+ const fs = require("node:fs");
6
+ const net = require("node:net");
6
7
  const collectExtractProps_1 = require("./requests/collectExtractProps");
7
8
  const componentInfos_1 = require("./requests/componentInfos");
8
9
  const getImportPathForFile_1 = require("./requests/getImportPathForFile");
@@ -10,82 +11,55 @@ const getPropertiesAtLocation_1 = require("./requests/getPropertiesAtLocation");
10
11
  const getQuickInfoAtPosition_1 = require("./requests/getQuickInfoAtPosition");
11
12
  const utils_1 = require("./utils");
12
13
  async function startNamedPipeServer(ts, info, language, projectKind) {
14
+ let lastProjectVersion;
15
+ const requestContext = {
16
+ typescript: ts,
17
+ languageService: info.languageService,
18
+ languageServiceHost: info.languageServiceHost,
19
+ language: language,
20
+ isTsPlugin: true,
21
+ getFileId: (fileName) => fileName,
22
+ };
23
+ const dataChunks = [];
24
+ const currentData = new language_core_1.FileMap(false);
25
+ const allConnections = new Set();
26
+ const pendingRequests = new Set();
13
27
  const server = net.createServer(connection => {
14
- connection.on('data', data => {
15
- const text = data.toString();
16
- if (text === 'ping') {
17
- connection.write('pong');
18
- return;
19
- }
20
- const request = JSON.parse(text);
21
- const fileName = request.args[0];
22
- const requestContext = {
23
- typescript: ts,
24
- languageService: info.languageService,
25
- languageServiceHost: info.languageServiceHost,
26
- language: language,
27
- isTsPlugin: true,
28
- getFileId: (fileName) => fileName,
29
- };
30
- if (request.type === 'containsFile') {
31
- sendResponse(info.project.containsFile(ts.server.toNormalizedPath(fileName)));
32
- }
33
- else if (request.type === 'projectInfo') {
34
- sendResponse({
35
- name: info.project.getProjectName(),
36
- kind: info.project.projectKind,
37
- currentDirectory: info.project.getCurrentDirectory(),
38
- });
39
- }
40
- else if (request.type === 'collectExtractProps') {
41
- const result = collectExtractProps_1.collectExtractProps.apply(requestContext, request.args);
42
- sendResponse(result);
43
- }
44
- else if (request.type === 'getImportPathForFile') {
45
- const result = getImportPathForFile_1.getImportPathForFile.apply(requestContext, request.args);
46
- sendResponse(result);
47
- }
48
- else if (request.type === 'getPropertiesAtLocation') {
49
- const result = getPropertiesAtLocation_1.getPropertiesAtLocation.apply(requestContext, request.args);
50
- sendResponse(result);
51
- }
52
- else if (request.type === 'getQuickInfoAtPosition') {
53
- const result = getQuickInfoAtPosition_1.getQuickInfoAtPosition.apply(requestContext, request.args);
54
- sendResponse(result);
55
- }
56
- // Component Infos
57
- else if (request.type === 'getComponentProps') {
58
- const result = componentInfos_1.getComponentProps.apply(requestContext, request.args);
59
- sendResponse(result);
60
- }
61
- else if (request.type === 'getComponentEvents') {
62
- const result = componentInfos_1.getComponentEvents.apply(requestContext, request.args);
63
- sendResponse(result);
64
- }
65
- else if (request.type === 'getTemplateContextProps') {
66
- const result = componentInfos_1.getTemplateContextProps.apply(requestContext, request.args);
67
- sendResponse(result);
68
- }
69
- else if (request.type === 'getComponentNames') {
70
- const result = componentInfos_1.getComponentNames.apply(requestContext, request.args);
71
- sendResponse(result);
72
- }
73
- else if (request.type === 'getElementAttrs') {
74
- const result = componentInfos_1.getElementAttrs.apply(requestContext, request.args);
75
- sendResponse(result);
76
- }
77
- else {
78
- console.warn('[Vue Named Pipe Server] Unknown request type:', request.type);
28
+ allConnections.add(connection);
29
+ connection.on('end', () => {
30
+ allConnections.delete(connection);
31
+ });
32
+ connection.on('data', buffer => {
33
+ dataChunks.push(buffer);
34
+ const text = dataChunks.toString();
35
+ if (text.endsWith('\n\n')) {
36
+ dataChunks.length = 0;
37
+ const requests = text.split('\n\n');
38
+ for (let json of requests) {
39
+ json = json.trim();
40
+ if (!json) {
41
+ continue;
42
+ }
43
+ try {
44
+ onRequest(connection, JSON.parse(json));
45
+ }
46
+ catch (e) {
47
+ console.error('[Vue Named Pipe Server] JSON parse error:', e);
48
+ }
49
+ }
79
50
  }
80
51
  });
81
52
  connection.on('error', err => console.error('[Vue Named Pipe Server]', err.message));
82
- function sendResponse(data) {
83
- connection.write(JSON.stringify(data ?? null) + '\n\n');
53
+ for (const [fileName, [componentNames, componentProps]] of currentData) {
54
+ notify(connection, 'componentNamesUpdated', fileName, Object.keys(componentNames));
55
+ for (const [name, props] of Object.entries(componentProps)) {
56
+ notify(connection, 'componentPropsUpdated', fileName, [name, props]);
57
+ }
84
58
  }
85
59
  });
86
- for (let i = 0; i < 20; i++) {
87
- const path = (0, utils_1.getNamedPipePath)(projectKind, i);
88
- const socket = await (0, utils_1.connect)(path, 100);
60
+ for (let i = 0; i < 10; i++) {
61
+ const path = (0, utils_1.getServerPath)(projectKind, i);
62
+ const socket = await connect(path, 100);
89
63
  if (typeof socket === 'object') {
90
64
  socket.end();
91
65
  }
@@ -98,6 +72,164 @@ async function startNamedPipeServer(ts, info, language, projectKind) {
98
72
  break;
99
73
  }
100
74
  }
75
+ updateWhile();
76
+ async function updateWhile() {
77
+ while (true) {
78
+ await sleep(500);
79
+ const projectVersion = info.project.getProjectVersion();
80
+ if (lastProjectVersion === projectVersion) {
81
+ continue;
82
+ }
83
+ const connections = [...allConnections].filter(c => !c.destroyed);
84
+ if (!connections.length) {
85
+ continue;
86
+ }
87
+ const token = info.languageServiceHost.getCancellationToken?.();
88
+ const openedScriptInfos = info.project.getRootScriptInfos().filter(info => info.isScriptOpen());
89
+ if (!openedScriptInfos.length) {
90
+ continue;
91
+ }
92
+ for (const scriptInfo of openedScriptInfos) {
93
+ await sleep(10);
94
+ if (token?.isCancellationRequested()) {
95
+ break;
96
+ }
97
+ let data = currentData.get(scriptInfo.fileName);
98
+ if (!data) {
99
+ data = [[], {}];
100
+ currentData.set(scriptInfo.fileName, data);
101
+ }
102
+ const [oldComponentNames, componentProps] = data;
103
+ const newComponentNames = componentInfos_1.getComponentNames.apply(requestContext, [scriptInfo.fileName]) ?? [];
104
+ if (JSON.stringify(oldComponentNames) !== JSON.stringify(newComponentNames)) {
105
+ data[0] = newComponentNames;
106
+ for (const connection of connections) {
107
+ notify(connection, 'componentNamesUpdated', scriptInfo.fileName, newComponentNames);
108
+ }
109
+ }
110
+ for (const [name, props] of Object.entries(componentProps)) {
111
+ await sleep(10);
112
+ if (token?.isCancellationRequested()) {
113
+ break;
114
+ }
115
+ const newProps = componentInfos_1.getComponentProps.apply(requestContext, [scriptInfo.fileName, name]) ?? [];
116
+ if (JSON.stringify(props) !== JSON.stringify(newProps)) {
117
+ componentProps[name] = newProps;
118
+ for (const connection of connections) {
119
+ notify(connection, 'componentPropsUpdated', scriptInfo.fileName, [name, newProps]);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ lastProjectVersion = projectVersion;
125
+ }
126
+ }
127
+ function sleep(ms) {
128
+ return new Promise(resolve => setTimeout(resolve, ms));
129
+ }
130
+ function notify(connection, type, fileName, data) {
131
+ connection.write(JSON.stringify([type, fileName, data]) + '\n\n');
132
+ }
133
+ function onRequest(connection, [seq, requestType, ...args]) {
134
+ if (pendingRequests.has(seq)) {
135
+ return;
136
+ }
137
+ setTimeout(() => pendingRequests.delete(seq), 500);
138
+ pendingRequests.add(seq);
139
+ let data;
140
+ try {
141
+ data = handleRequest(requestType, ...args);
142
+ }
143
+ catch {
144
+ data = null;
145
+ }
146
+ connection.write(JSON.stringify([seq, data ?? null]) + '\n\n');
147
+ }
148
+ function handleRequest(requestType, ...args) {
149
+ const fileName = args[0];
150
+ if (requestType === 'projectInfo') {
151
+ return {
152
+ name: info.project.getProjectName(),
153
+ kind: info.project.projectKind,
154
+ currentDirectory: info.project.getCurrentDirectory(),
155
+ };
156
+ }
157
+ else if (requestType === 'containsFile') {
158
+ return info.project.containsFile(ts.server.toNormalizedPath(fileName));
159
+ }
160
+ else if (requestType === 'collectExtractProps') {
161
+ return collectExtractProps_1.collectExtractProps.apply(requestContext, args);
162
+ }
163
+ else if (requestType === 'getImportPathForFile') {
164
+ return getImportPathForFile_1.getImportPathForFile.apply(requestContext, args);
165
+ }
166
+ else if (requestType === 'getPropertiesAtLocation') {
167
+ return getPropertiesAtLocation_1.getPropertiesAtLocation.apply(requestContext, args);
168
+ }
169
+ else if (requestType === 'getQuickInfoAtPosition') {
170
+ return getQuickInfoAtPosition_1.getQuickInfoAtPosition.apply(requestContext, args);
171
+ }
172
+ else if (requestType === 'subscribeComponentProps') {
173
+ const tag = args[1];
174
+ const props = componentInfos_1.getComponentProps.apply(requestContext, [fileName, tag]) ?? [];
175
+ let data = currentData.get(fileName);
176
+ if (!data) {
177
+ data = [[], {}];
178
+ currentData.set(fileName, data);
179
+ }
180
+ data[1][tag] = props;
181
+ return props;
182
+ }
183
+ else if (requestType === 'getComponentEvents') {
184
+ return componentInfos_1.getComponentEvents.apply(requestContext, args);
185
+ }
186
+ else if (requestType === 'getTemplateContextProps') {
187
+ return componentInfos_1.getTemplateContextProps.apply(requestContext, args);
188
+ }
189
+ else if (requestType === 'getElementAttrs') {
190
+ return componentInfos_1.getElementAttrs.apply(requestContext, args);
191
+ }
192
+ console.warn('[Vue Named Pipe Server] Unknown request:', requestType);
193
+ debugger;
194
+ return undefined;
195
+ }
196
+ }
197
+ function connect(namedPipePath, timeout) {
198
+ return new Promise(resolve => {
199
+ const socket = net.connect(namedPipePath);
200
+ if (timeout) {
201
+ socket.setTimeout(timeout);
202
+ }
203
+ const onConnect = () => {
204
+ cleanup();
205
+ resolve(socket);
206
+ };
207
+ const onError = (err) => {
208
+ if (err.code === 'ECONNREFUSED') {
209
+ try {
210
+ console.log('[Vue Named Pipe Client] Deleting:', namedPipePath);
211
+ fs.promises.unlink(namedPipePath);
212
+ }
213
+ catch { }
214
+ }
215
+ cleanup();
216
+ resolve('error');
217
+ socket.end();
218
+ };
219
+ const onTimeout = () => {
220
+ cleanup();
221
+ resolve('timeout');
222
+ socket.end();
223
+ };
224
+ const cleanup = () => {
225
+ socket.off('connect', onConnect);
226
+ socket.off('error', onError);
227
+ socket.off('timeout', onTimeout);
228
+ };
229
+ socket.on('connect', onConnect);
230
+ socket.on('error', onError);
231
+ socket.on('timeout', onTimeout);
232
+ });
101
233
  }
102
234
  function tryListen(server, namedPipePath) {
103
235
  return new Promise(resolve => {
package/lib/utils.d.ts CHANGED
@@ -1,16 +1,38 @@
1
- import * as net from 'net';
1
+ import { FileMap } from '@vue/language-core';
2
+ import * as net from 'node:net';
2
3
  import type * as ts from 'typescript';
3
- import type { ProjectInfo, Request } from './server';
4
+ import type { NotificationData, ProjectInfo, RequestData } from './server';
4
5
  export { TypeScriptProjectHost } from '@volar/typescript';
5
- export declare const onSomePipeReadyCallbacks: (() => void)[];
6
- export declare function getNamedPipePath(projectKind: ts.server.ProjectKind.Configured | ts.server.ProjectKind.Inferred, key: number): string;
7
- export declare function getReadyNamedPipePaths(): {
8
- configured: string[];
9
- inferred: string[];
10
- };
11
- export declare function connect(namedPipePath: string, timeout?: number): Promise<net.Socket | "error" | "timeout">;
12
- export declare function searchNamedPipeServerForFile(fileName: string): Promise<{
13
- socket: net.Socket;
14
- projectInfo: ProjectInfo;
15
- } | undefined>;
16
- export declare function sendRequestWorker<T>(request: Request, socket: net.Socket): Promise<T | null | undefined>;
6
+ export declare function getServerPath(kind: ts.server.ProjectKind, id: number): string;
7
+ declare class NamedPipeServer {
8
+ path: string;
9
+ connecting: boolean;
10
+ projectInfo?: ProjectInfo;
11
+ containsFileCache: Map<string, Promise<boolean | null | undefined>>;
12
+ componentNamesAndProps: FileMap<Record<string, {
13
+ name: string;
14
+ required?: true;
15
+ commentMarkdown?: string;
16
+ }[] | null>>;
17
+ constructor(kind: ts.server.ProjectKind, id: number);
18
+ containsFile(fileName: string): Promise<boolean | null | undefined> | undefined;
19
+ getComponentProps(fileName: string, tag: string): Promise<{
20
+ name: string;
21
+ required?: true;
22
+ commentMarkdown?: string;
23
+ }[] | null | undefined>;
24
+ update(): void;
25
+ connect(): void;
26
+ close(): void;
27
+ socket?: net.Socket;
28
+ seq: number;
29
+ dataChunks: Buffer[];
30
+ requestHandlers: Map<number, (res: any) => void>;
31
+ onData(chunk: Buffer): void;
32
+ onNotification(type: NotificationData[0], fileName: string, data: any): void;
33
+ sendRequest<T>(requestType: RequestData[1], fileName: string, ...args: any[]): Promise<T | null | undefined>;
34
+ }
35
+ export declare const configuredServers: NamedPipeServer[];
36
+ export declare const inferredServers: NamedPipeServer[];
37
+ export declare const onServerReady: (() => void)[];
38
+ export declare function getBestServer(fileName: string): Promise<NamedPipeServer | undefined>;
package/lib/utils.js CHANGED
@@ -1,195 +1,232 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.onSomePipeReadyCallbacks = void 0;
4
- exports.getNamedPipePath = getNamedPipePath;
5
- exports.getReadyNamedPipePaths = getReadyNamedPipePaths;
6
- exports.connect = connect;
7
- exports.searchNamedPipeServerForFile = searchNamedPipeServerForFile;
8
- exports.sendRequestWorker = sendRequestWorker;
9
- const fs = require("fs");
10
- const net = require("net");
11
- const os = require("os");
12
- const path = require("path");
3
+ exports.onServerReady = exports.inferredServers = exports.configuredServers = void 0;
4
+ exports.getServerPath = getServerPath;
5
+ exports.getBestServer = getBestServer;
6
+ const language_core_1 = require("@vue/language-core");
7
+ const shared_1 = require("@vue/shared");
8
+ const fs = require("node:fs");
9
+ const net = require("node:net");
10
+ const os = require("node:os");
11
+ const path = require("node:path");
13
12
  const { version } = require('../package.json');
14
13
  const platform = os.platform();
15
14
  const pipeDir = platform === 'win32'
16
- ? `\\\\.\\pipe`
17
- : `/tmp`;
18
- const toFullPath = (file) => {
19
- if (platform === 'win32') {
20
- return pipeDir + '\\' + file;
15
+ ? `\\\\.\\pipe\\`
16
+ : `/tmp/`;
17
+ function getServerPath(kind, id) {
18
+ if (kind === 1) {
19
+ return `${pipeDir}vue-named-pipe-${version}-configured-${id}`;
21
20
  }
22
21
  else {
23
- return pipeDir + '/' + file;
24
- }
25
- };
26
- const configuredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-configured-`);
27
- const inferredNamedPipePathPrefix = toFullPath(`vue-named-pipe-${version}-inferred-`);
28
- const pipes = new Map();
29
- exports.onSomePipeReadyCallbacks = [];
30
- function waitingForNamedPipeServerReady(namedPipePath) {
31
- const socket = net.connect(namedPipePath);
32
- const start = Date.now();
33
- socket.on('connect', () => {
34
- console.log('[Vue Named Pipe Client] Connected:', namedPipePath, 'in', (Date.now() - start) + 'ms');
35
- socket.write('ping');
36
- });
37
- socket.on('data', () => {
38
- console.log('[Vue Named Pipe Client] Ready:', namedPipePath, 'in', (Date.now() - start) + 'ms');
39
- pipes.set(namedPipePath, 'ready');
40
- socket.end();
41
- exports.onSomePipeReadyCallbacks.forEach(cb => cb());
42
- });
43
- socket.on('error', err => {
44
- if (err.code === 'ECONNREFUSED') {
45
- try {
46
- console.log('[Vue Named Pipe Client] Deleting:', namedPipePath);
47
- fs.promises.unlink(namedPipePath);
48
- }
49
- catch { }
50
- }
51
- pipes.delete(namedPipePath);
52
- socket.end();
53
- });
54
- socket.on('timeout', () => {
55
- pipes.delete(namedPipePath);
56
- socket.end();
57
- });
58
- }
59
- function getNamedPipePath(projectKind, key) {
60
- return projectKind === 1
61
- ? `${configuredNamedPipePathPrefix}${key}`
62
- : `${inferredNamedPipePathPrefix}${key}`;
22
+ return `${pipeDir}vue-named-pipe-${version}-inferred-${id}`;
23
+ }
63
24
  }
64
- function getReadyNamedPipePaths() {
65
- const configuredPipes = [];
66
- const inferredPipes = [];
67
- for (let i = 0; i < 20; i++) {
68
- const configuredPipe = getNamedPipePath(1, i);
69
- const inferredPipe = getNamedPipePath(0, i);
70
- if (pipes.get(configuredPipe) === 'ready') {
71
- configuredPipes.push(configuredPipe);
72
- }
73
- else if (!pipes.has(configuredPipe)) {
74
- pipes.set(configuredPipe, 'unknown');
75
- waitingForNamedPipeServerReady(configuredPipe);
25
+ class NamedPipeServer {
26
+ constructor(kind, id) {
27
+ this.connecting = false;
28
+ this.containsFileCache = new Map();
29
+ this.componentNamesAndProps = new language_core_1.FileMap(false);
30
+ this.seq = 0;
31
+ this.dataChunks = [];
32
+ this.requestHandlers = new Map();
33
+ this.path = getServerPath(kind, id);
34
+ }
35
+ containsFile(fileName) {
36
+ if (this.projectInfo) {
37
+ if (!this.containsFileCache.has(fileName)) {
38
+ this.containsFileCache.set(fileName, (async () => {
39
+ const res = await this.sendRequest('containsFile', fileName);
40
+ if (typeof res !== 'boolean') {
41
+ // If the request fails, delete the cache
42
+ this.containsFileCache.delete(fileName);
43
+ }
44
+ return res;
45
+ })());
46
+ }
47
+ return this.containsFileCache.get(fileName);
76
48
  }
77
- if (pipes.get(inferredPipe) === 'ready') {
78
- inferredPipes.push(inferredPipe);
49
+ }
50
+ async getComponentProps(fileName, tag) {
51
+ const componentAndProps = this.componentNamesAndProps.get(fileName);
52
+ if (!componentAndProps) {
53
+ return;
79
54
  }
80
- else if (!pipes.has(inferredPipe)) {
81
- pipes.set(inferredPipe, 'unknown');
82
- waitingForNamedPipeServerReady(inferredPipe);
55
+ const props = componentAndProps[tag]
56
+ ?? componentAndProps[(0, shared_1.camelize)(tag)]
57
+ ?? componentAndProps[(0, shared_1.capitalize)((0, shared_1.camelize)(tag))];
58
+ if (props) {
59
+ return props;
83
60
  }
61
+ return await this.sendRequest('subscribeComponentProps', fileName, tag);
84
62
  }
85
- return {
86
- configured: configuredPipes,
87
- inferred: inferredPipes,
88
- };
89
- }
90
- function connect(namedPipePath, timeout) {
91
- return new Promise(resolve => {
92
- const socket = net.connect(namedPipePath);
93
- if (timeout) {
94
- socket.setTimeout(timeout);
63
+ update() {
64
+ if (!this.connecting && !this.projectInfo) {
65
+ this.connecting = true;
66
+ this.connect();
95
67
  }
96
- const onConnect = () => {
97
- cleanup();
98
- resolve(socket);
99
- };
100
- const onError = (err) => {
68
+ }
69
+ connect() {
70
+ this.socket = net.connect(this.path);
71
+ this.socket.on('data', this.onData.bind(this));
72
+ this.socket.on('connect', async () => {
73
+ const projectInfo = await this.sendRequest('projectInfo', '');
74
+ if (projectInfo) {
75
+ console.log('TSServer project ready:', projectInfo.name);
76
+ this.projectInfo = projectInfo;
77
+ this.containsFileCache.clear();
78
+ exports.onServerReady.forEach(cb => cb());
79
+ }
80
+ else {
81
+ this.close();
82
+ }
83
+ });
84
+ this.socket.on('error', err => {
101
85
  if (err.code === 'ECONNREFUSED') {
102
86
  try {
103
- console.log('[Vue Named Pipe Client] Deleting:', namedPipePath);
104
- fs.promises.unlink(namedPipePath);
87
+ console.log('Deleteing invalid named pipe file:', this.path);
88
+ fs.promises.unlink(this.path);
105
89
  }
106
90
  catch { }
107
91
  }
108
- pipes.delete(namedPipePath);
109
- cleanup();
110
- resolve('error');
111
- socket.end();
112
- };
113
- const onTimeout = () => {
114
- cleanup();
115
- resolve('timeout');
116
- socket.end();
117
- };
118
- const cleanup = () => {
119
- socket.off('connect', onConnect);
120
- socket.off('error', onError);
121
- socket.off('timeout', onTimeout);
122
- };
123
- socket.on('connect', onConnect);
124
- socket.on('error', onError);
125
- socket.on('timeout', onTimeout);
126
- });
92
+ this.close();
93
+ });
94
+ this.socket.on('timeout', () => {
95
+ this.close();
96
+ });
97
+ }
98
+ close() {
99
+ this.connecting = false;
100
+ this.projectInfo = undefined;
101
+ this.socket?.end();
102
+ }
103
+ onData(chunk) {
104
+ this.dataChunks.push(chunk);
105
+ const data = Buffer.concat(this.dataChunks);
106
+ const text = data.toString();
107
+ if (text.endsWith('\n\n')) {
108
+ this.dataChunks.length = 0;
109
+ const results = text.split('\n\n');
110
+ for (let result of results) {
111
+ result = result.trim();
112
+ if (!result) {
113
+ continue;
114
+ }
115
+ try {
116
+ const data = JSON.parse(result.trim());
117
+ if (typeof data[0] === 'number') {
118
+ const [seq, res] = data;
119
+ this.requestHandlers.get(seq)?.(res);
120
+ }
121
+ else {
122
+ const [type, fileName, res] = data;
123
+ this.onNotification(type, fileName, res);
124
+ }
125
+ }
126
+ catch (e) {
127
+ console.error('JSON parse error:', e);
128
+ }
129
+ }
130
+ }
131
+ }
132
+ onNotification(type, fileName, data) {
133
+ // console.log(`[${type}] ${fileName} ${JSON.stringify(data)}`);
134
+ if (type === 'componentNamesUpdated') {
135
+ let components = this.componentNamesAndProps.get(fileName);
136
+ if (!components) {
137
+ components = {};
138
+ this.componentNamesAndProps.set(fileName, components);
139
+ }
140
+ const newNames = data;
141
+ const newNameSet = new Set(newNames);
142
+ for (const name in components) {
143
+ if (!newNameSet.has(name)) {
144
+ delete components[name];
145
+ }
146
+ }
147
+ for (const name of newNames) {
148
+ if (!components[name]) {
149
+ components[name] = null;
150
+ }
151
+ }
152
+ }
153
+ else if (type === 'componentPropsUpdated') {
154
+ const components = this.componentNamesAndProps.get(fileName) ?? {};
155
+ const [name, props] = data;
156
+ components[name] = props;
157
+ }
158
+ else {
159
+ console.error('Unknown notification type:', type);
160
+ debugger;
161
+ }
162
+ }
163
+ sendRequest(requestType, fileName, ...args) {
164
+ return new Promise(resolve => {
165
+ const seq = this.seq++;
166
+ // console.time(`[${seq}] ${requestType} ${fileName}`);
167
+ this.requestHandlers.set(seq, data => {
168
+ // console.timeEnd(`[${seq}] ${requestType} ${fileName}`);
169
+ this.requestHandlers.delete(seq);
170
+ resolve(data);
171
+ clearInterval(retryTimer);
172
+ });
173
+ const retry = () => {
174
+ const data = [seq, requestType, fileName, ...args];
175
+ this.socket.write(JSON.stringify(data) + '\n\n');
176
+ };
177
+ retry();
178
+ const retryTimer = setInterval(retry, 1000);
179
+ });
180
+ }
127
181
  }
128
- async function searchNamedPipeServerForFile(fileName) {
129
- const paths = await getReadyNamedPipePaths();
130
- const configuredServers = (await Promise.all(paths.configured.map(async (path) => {
131
- // Find existing servers
132
- const socket = await connect(path);
133
- if (typeof socket !== 'object') {
182
+ exports.configuredServers = [];
183
+ exports.inferredServers = [];
184
+ exports.onServerReady = [];
185
+ for (let i = 0; i < 10; i++) {
186
+ exports.configuredServers.push(new NamedPipeServer(1, i));
187
+ exports.inferredServers.push(new NamedPipeServer(0, i));
188
+ }
189
+ async function getBestServer(fileName) {
190
+ for (const server of exports.configuredServers) {
191
+ server.update();
192
+ }
193
+ let servers = (await Promise.all(exports.configuredServers.map(async (server) => {
194
+ const projectInfo = server.projectInfo;
195
+ if (!projectInfo) {
134
196
  return;
135
197
  }
136
- // Find servers containing the current file
137
- const containsFile = await sendRequestWorker({ type: 'containsFile', args: [fileName] }, socket);
198
+ const containsFile = await server.containsFile(fileName);
138
199
  if (!containsFile) {
139
- socket.end();
140
- return;
141
- }
142
- // Get project info for each server
143
- const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket);
144
- if (!projectInfo) {
145
- socket.end();
146
200
  return;
147
201
  }
148
- return {
149
- socket,
150
- projectInfo,
151
- };
202
+ return server;
152
203
  }))).filter(server => !!server);
153
204
  // Sort servers by tsconfig
154
- configuredServers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo.name, b.projectInfo.name));
155
- if (configuredServers.length) {
156
- // Close all but the first server
157
- for (let i = 1; i < configuredServers.length; i++) {
158
- configuredServers[i].socket.end();
159
- }
205
+ servers.sort((a, b) => sortTSConfigs(fileName, a.projectInfo.name, b.projectInfo.name));
206
+ if (servers.length) {
160
207
  // Return the first server
161
- return configuredServers[0];
208
+ return servers[0];
162
209
  }
163
- const inferredServers = (await Promise.all(paths.inferred.map(async (namedPipePath) => {
164
- // Find existing servers
165
- const socket = await connect(namedPipePath);
166
- if (typeof socket !== 'object') {
167
- return;
168
- }
169
- // Get project info for each server
170
- const projectInfo = await sendRequestWorker({ type: 'projectInfo', args: [fileName] }, socket);
210
+ for (const server of exports.inferredServers) {
211
+ server.update();
212
+ }
213
+ servers = (await Promise.all(exports.inferredServers.map(server => {
214
+ const projectInfo = server.projectInfo;
171
215
  if (!projectInfo) {
172
- socket.end();
173
216
  return;
174
217
  }
175
218
  // Check if the file is in the project's directory
176
- if (!path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) {
177
- return {
178
- socket,
179
- projectInfo,
180
- };
219
+ if (path.relative(projectInfo.currentDirectory, fileName).startsWith('..')) {
220
+ return;
181
221
  }
222
+ return server;
182
223
  }))).filter(server => !!server);
183
224
  // Sort servers by directory
184
- inferredServers.sort((a, b) => b.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length
225
+ servers.sort((a, b) => b.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length
185
226
  - a.projectInfo.currentDirectory.replace(/\\/g, '/').split('/').length);
186
- if (inferredServers.length) {
187
- // Close all but the first server
188
- for (let i = 1; i < inferredServers.length; i++) {
189
- inferredServers[i].socket.end();
190
- }
227
+ if (servers.length) {
191
228
  // Return the first server
192
- return inferredServers[0];
229
+ return servers[0];
193
230
  }
194
231
  }
195
232
  function sortTSConfigs(file, a, b) {
@@ -208,37 +245,4 @@ function isFileInDir(fileName, dir) {
208
245
  const relative = path.relative(dir, fileName);
209
246
  return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative);
210
247
  }
211
- function sendRequestWorker(request, socket) {
212
- return new Promise(resolve => {
213
- let dataChunks = [];
214
- const onData = (chunk) => {
215
- dataChunks.push(chunk);
216
- const data = Buffer.concat(dataChunks);
217
- const text = data.toString();
218
- if (text.endsWith('\n\n')) {
219
- let json = null;
220
- try {
221
- json = JSON.parse(text);
222
- }
223
- catch (e) {
224
- console.error('[Vue Named Pipe Client] Failed to parse response:', text);
225
- }
226
- cleanup();
227
- resolve(json);
228
- }
229
- };
230
- const onError = (err) => {
231
- console.error('[Vue Named Pipe Client] Error:', err.message);
232
- cleanup();
233
- resolve(undefined);
234
- };
235
- const cleanup = () => {
236
- socket.off('data', onData);
237
- socket.off('error', onError);
238
- };
239
- socket.on('data', onData);
240
- socket.on('error', onError);
241
- socket.write(JSON.stringify(request));
242
- });
243
- }
244
248
  //# sourceMappingURL=utils.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vue/typescript-plugin",
3
- "version": "2.1.10",
3
+ "version": "2.2.0",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "**/*.js",
@@ -13,12 +13,12 @@
13
13
  "directory": "packages/typescript-plugin"
14
14
  },
15
15
  "dependencies": {
16
- "@volar/typescript": "~2.4.8",
17
- "@vue/language-core": "2.1.10",
16
+ "@volar/typescript": "~2.4.11",
17
+ "@vue/language-core": "2.2.0",
18
18
  "@vue/shared": "^3.5.0"
19
19
  },
20
20
  "devDependencies": {
21
21
  "@types/node": "latest"
22
22
  },
23
- "gitHead": "b0af30caee2f8dfb1a8393c1b400f38e31fa4883"
23
+ "gitHead": "5babca774658d4b9afbe877ac7c8cafdaecf2c3e"
24
24
  }