vovk-cli 0.0.1-beta.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.
Files changed (82) hide show
  1. package/.eslintrc.js +20 -0
  2. package/README.md +1 -0
  3. package/dist/getProjectInfo/directoryExists.d.ts +1 -0
  4. package/dist/getProjectInfo/directoryExists.js +16 -0
  5. package/dist/getProjectInfo/getConfig.d.ts +7 -0
  6. package/dist/getProjectInfo/getConfig.js +29 -0
  7. package/dist/getProjectInfo/getCwdPath.d.ts +1 -0
  8. package/dist/getProjectInfo/getCwdPath.js +19 -0
  9. package/dist/getProjectInfo/getSrcRoot.d.ts +1 -0
  10. package/dist/getProjectInfo/getSrcRoot.js +19 -0
  11. package/dist/getProjectInfo/index.d.ts +48 -0
  12. package/dist/getProjectInfo/index.js +78 -0
  13. package/dist/getProjectInfo/readConfig.d.ts +3 -0
  14. package/dist/getProjectInfo/readConfig.js +73 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +104 -0
  17. package/dist/init.d.ts +2 -0
  18. package/dist/init.js +173 -0
  19. package/dist/locateSegments.d.ts +5 -0
  20. package/dist/locateSegments.js +58 -0
  21. package/dist/postinstall.d.ts +1 -0
  22. package/dist/postinstall.js +27 -0
  23. package/dist/server/createMetadataServer.d.ts +5 -0
  24. package/dist/server/createMetadataServer.js +31 -0
  25. package/dist/server/diffMetadata.d.ts +43 -0
  26. package/dist/server/diffMetadata.js +77 -0
  27. package/dist/server/ensureMetadataFiles.d.ts +3 -0
  28. package/dist/server/ensureMetadataFiles.js +100 -0
  29. package/dist/server/generateClient.d.ts +7 -0
  30. package/dist/server/generateClient.js +98 -0
  31. package/dist/server/index.d.ts +6 -0
  32. package/dist/server/index.js +285 -0
  33. package/dist/server/isMetadataEmpty.d.ts +2 -0
  34. package/dist/server/isMetadataEmpty.js +7 -0
  35. package/dist/server/logDiffResult.d.ts +3 -0
  36. package/dist/server/logDiffResult.js +84 -0
  37. package/dist/server/writeOneMetadataFile.d.ts +11 -0
  38. package/dist/server/writeOneMetadataFile.js +34 -0
  39. package/dist/types.d.ts +30 -0
  40. package/dist/types.js +2 -0
  41. package/dist/utils/debounceWithArgs.d.ts +2 -0
  42. package/dist/utils/debounceWithArgs.js +20 -0
  43. package/dist/utils/fileExists.d.ts +1 -0
  44. package/dist/utils/fileExists.js +16 -0
  45. package/dist/utils/getAvailablePort.d.ts +10 -0
  46. package/dist/utils/getAvailablePort.js +47 -0
  47. package/package.json +43 -0
  48. package/src/getProjectInfo/directoryExists.ts +10 -0
  49. package/src/getProjectInfo/getConfig.ts +29 -0
  50. package/src/getProjectInfo/getCwdPath.ts +15 -0
  51. package/src/getProjectInfo/getSrcRoot.ts +14 -0
  52. package/src/getProjectInfo/index.ts +63 -0
  53. package/src/getProjectInfo/readConfig.ts +50 -0
  54. package/src/index.ts +112 -0
  55. package/src/init.ts +174 -0
  56. package/src/locateSegments.ts +40 -0
  57. package/src/postinstall.ts +27 -0
  58. package/src/server/createMetadataServer.ts +30 -0
  59. package/src/server/diffMetadata.ts +110 -0
  60. package/src/server/ensureMetadataFiles.ts +92 -0
  61. package/src/server/generateClient.ts +108 -0
  62. package/src/server/index.ts +306 -0
  63. package/src/server/isMetadataEmpty.ts +6 -0
  64. package/src/server/logDiffResult.ts +114 -0
  65. package/src/server/writeOneMetadataFile.ts +44 -0
  66. package/src/types.ts +58 -0
  67. package/src/utils/debounceWithArgs.ts +22 -0
  68. package/src/utils/fileExists.ts +10 -0
  69. package/src/utils/getAvailablePort.ts +50 -0
  70. package/test/data/segments/[[...vovk]]/route.ts +0 -0
  71. package/test/data/segments/bar/[[...custom]]/route.ts +0 -0
  72. package/test/data/segments/baz/[[...vovk]]/noroute.ts +0 -0
  73. package/test/data/segments/foo/[[...vovk]]/route.ts +0 -0
  74. package/test/data/segments/garply/waldo/route.ts +0 -0
  75. package/test/data/segments/grault/xxxx/[[...vovk]]/noroute.ts +0 -0
  76. package/test/data/segments/quux/corge/[[...vovk]]/route.ts +0 -0
  77. package/test/index.ts +3 -0
  78. package/test/metadata-diff.test.ts +300 -0
  79. package/test/metadata-write.test.ts +82 -0
  80. package/test/utils.test.ts +49 -0
  81. package/tsconfig.json +11 -0
  82. package/tsconfig.test.json +4 -0
@@ -0,0 +1,92 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ import debounce from 'lodash/debounce';
4
+ import writeOneMetadataFile, { ROOT_SEGMENT_SCHEMA_NAME } from './writeOneMetadataFile';
5
+ import { ProjectInfo } from '../getProjectInfo';
6
+
7
+ export default async function ensureMetadataFiles(
8
+ metadataOutFullPath: string,
9
+ segmentNames: string[],
10
+ projectInfo: ProjectInfo | null
11
+ ): Promise<void> {
12
+ const now = Date.now();
13
+ let hasChanged = false;
14
+ // Create index.js file
15
+ const indexContent = segmentNames
16
+ .map((segmentName) => {
17
+ return `module.exports['${segmentName}'] = require('./${segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json');`;
18
+ })
19
+ .join('\n');
20
+
21
+ const dTsContent = `import type { VovkMetadata } from 'vovk';
22
+ declare const segmentMetadata: Record<string, VovkMetadata>;
23
+ export default segmentMetadata;`;
24
+
25
+ await fs.mkdir(metadataOutFullPath, { recursive: true });
26
+ await fs.writeFile(path.join(metadataOutFullPath, 'index.js'), indexContent);
27
+ await fs.writeFile(path.join(metadataOutFullPath, 'index.d.ts'), dTsContent);
28
+
29
+ // Create JSON files (if not exist) with name [segmentName].json (where segmentName can include /, which means the folder structure can be nested) : {} (empty object)
30
+ await Promise.all(
31
+ segmentNames.map(async (segmentName) => {
32
+ const { isCreated } = await writeOneMetadataFile({
33
+ metadataOutFullPath,
34
+ metadata: {
35
+ emitMetadata: false,
36
+ segmentName,
37
+ controllers: {},
38
+ workers: {},
39
+ },
40
+ skipIfExists: true,
41
+ });
42
+
43
+ if (isCreated) {
44
+ projectInfo?.log.debug(`Created empty metadata file for segment "${segmentName}"`);
45
+ hasChanged = true;
46
+ }
47
+ })
48
+ );
49
+
50
+ // Recursive function to delete unnecessary JSON files and folders
51
+ async function deleteUnnecessaryJsonFiles(dirPath: string): Promise<void> {
52
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
53
+
54
+ await Promise.all(
55
+ entries.map(async (entry) => {
56
+ const fullPath = path.join(dirPath, entry.name);
57
+
58
+ if (entry.isDirectory()) {
59
+ // Recursively delete unnecessary files and folders within nested directories
60
+ await deleteUnnecessaryJsonFiles(fullPath);
61
+
62
+ // Check if the directory is empty after deletion and remove it if so
63
+ const remainingEntries = await fs.readdir(fullPath);
64
+ if (remainingEntries.length === 0) {
65
+ await fs.rmdir(fullPath);
66
+ projectInfo?.log.debug(`Deleted unnecessary metadata directory "${fullPath}"`);
67
+ hasChanged = true;
68
+ }
69
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
70
+ const relativePath = path.relative(metadataOutFullPath, fullPath);
71
+ const segmentName = relativePath.replace(/\\/g, '/').slice(0, -5); // Remove '.json' extension
72
+
73
+ if (
74
+ !segmentNames.includes(segmentName) &&
75
+ !segmentNames.includes(segmentName.replace(ROOT_SEGMENT_SCHEMA_NAME, ''))
76
+ ) {
77
+ await fs.unlink(fullPath);
78
+ projectInfo?.log.debug(`Deleted unnecessary metadata file for segment "${segmentName}"`);
79
+ hasChanged = true;
80
+ }
81
+ }
82
+ })
83
+ );
84
+ }
85
+
86
+ // Start the recursive deletion from the root directory
87
+ await deleteUnnecessaryJsonFiles(metadataOutFullPath);
88
+
89
+ if (hasChanged) projectInfo?.log.info(`Metadata files updated in ${Date.now() - now}ms`);
90
+ }
91
+
92
+ export const debouncedEnsureMetadataFiles = debounce(ensureMetadataFiles, 1000);
@@ -0,0 +1,108 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import type { ProjectInfo } from '../getProjectInfo';
4
+ import type { Segment } from '../locateSegments';
5
+ import { VovkMetadata } from 'vovk';
6
+
7
+ export default async function generateClient(
8
+ projectInfo: ProjectInfo,
9
+ segments: Segment[],
10
+ segmentsMetadata: Record<string, VovkMetadata>
11
+ ) {
12
+ const now = Date.now();
13
+ const outDir = projectInfo.clientOutFullPath;
14
+ const validatePath = projectInfo.config.validateOnClient?.startsWith('.')
15
+ ? path.join(projectInfo.cwd, projectInfo.config.validateOnClient)
16
+ : projectInfo.config.validateOnClient;
17
+ let dts = `// auto-generated
18
+ /* eslint-disable */
19
+ import type { clientizeController } from 'vovk/client';
20
+ import type { promisifyWorker } from 'vovk/worker';
21
+ import type { VovkClientFetcher } from 'vovk/client';
22
+ import type fetcher from '${projectInfo.fetcherClientImportPath}';
23
+
24
+ `;
25
+ let js = `// auto-generated
26
+ /* eslint-disable */
27
+ const { clientizeController } = require('vovk/client');
28
+ const { promisifyWorker } = require('vovk/worker');
29
+ const { default: fetcher } = require('${projectInfo.fetcherClientImportPath}');
30
+ const metadata = require('${projectInfo.metadataOutImportPath}');
31
+ `;
32
+ let ts = `// auto-generated
33
+ /* eslint-disable */
34
+ import { clientizeController } from 'vovk/client';
35
+ import { promisifyWorker } from 'vovk/worker';
36
+ import type { VovkClientFetcher } from 'vovk/client';
37
+ import fetcher from '${projectInfo.fetcherClientImportPath}';
38
+ import metadata from '${projectInfo.metadataOutImportPath}';
39
+
40
+ `;
41
+ for (let i = 0; i < segments.length; i++) {
42
+ const { routeFilePath, segmentName } = segments[i];
43
+ const metadata = segmentsMetadata[segmentName];
44
+ if (!metadata) {
45
+ throw new Error(`Unable to generate client. No metadata found for segment ${segmentName}`);
46
+ }
47
+ if (!metadata.emitMetadata) continue;
48
+ const importRouteFilePath = path.relative(projectInfo.config.clientOutDir, routeFilePath);
49
+
50
+ dts += `import type { Controllers as Controllers${i}, Workers as Workers${i} } from "${importRouteFilePath}";\n`;
51
+ ts += `import type { Controllers as Controllers${i}, Workers as Workers${i} } from "${importRouteFilePath}";\n`;
52
+ }
53
+
54
+ dts += `
55
+ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
56
+ `;
57
+ ts += `
58
+ ${validatePath ? `import validateOnClient from '${validatePath}';\n` : '\nconst validateOnClient = undefined;'}
59
+ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
60
+ const prefix = '${projectInfo.apiPrefix}';
61
+ `;
62
+ js += `
63
+ const { default: validateOnClient = null } = ${validatePath ? `require('${validatePath}')` : '{}'};
64
+ const prefix = '${projectInfo.apiPrefix}';
65
+ `;
66
+
67
+ for (let i = 0; i < segments.length; i++) {
68
+ const { segmentName } = segments[i];
69
+ const metadata = segmentsMetadata[segmentName];
70
+ if (!metadata) {
71
+ throw new Error(`Unable to generate client. No metadata found for segment ${segmentName}`);
72
+ }
73
+ if (!metadata.emitMetadata) continue;
74
+
75
+ for (const key of Object.keys(metadata.controllers)) {
76
+ dts += `export const ${key}: ReturnType<typeof clientizeController<Controllers${i}["${key}"], Options>>;\n`;
77
+ js += `exports.${key} = clientizeController(metadata['${segmentName}'].controllers.${key}, '${segmentName}', { fetcher, validateOnClient, defaultOptions: { prefix } });\n`;
78
+ ts += `export const ${key} = clientizeController<Controllers${i}["${key}"], Options>(metadata['${segmentName}'].controllers.${key}, '${segmentName}', { fetcher, validateOnClient, defaultOptions: { prefix } });\n`;
79
+ }
80
+
81
+ for (const key of Object.keys(metadata.workers)) {
82
+ dts += `export const ${key}: ReturnType<typeof promisifyWorker<Workers${i}["${key}"]>>;\n`;
83
+ js += `exports.${key} = promisifyWorker(null, metadata['${segmentName}'].workers.${key});\n`;
84
+ ts += `export const ${key} = promisifyWorker<Workers${i}["${key}"]>(null, metadata['${segmentName}'].workers.${key});\n`;
85
+ }
86
+ }
87
+
88
+ const localJsPath = path.join(outDir, 'client.js');
89
+ const localDtsPath = path.join(outDir, 'client.d.ts');
90
+ const localTsPath = path.join(outDir, 'index.ts');
91
+ const existingJs = await fs.readFile(localJsPath, 'utf-8').catch(() => '');
92
+ const existingDts = await fs.readFile(localDtsPath, 'utf-8').catch(() => '');
93
+ const existingTs = await fs.readFile(localTsPath, 'utf-8').catch(() => '');
94
+
95
+ if (existingJs === js && existingDts === dts && existingTs === ts) {
96
+ projectInfo.log.info(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms).`);
97
+ return { written: false, path: outDir };
98
+ }
99
+
100
+ await fs.mkdir(outDir, { recursive: true });
101
+ await fs.writeFile(localJsPath, js);
102
+ await fs.writeFile(localDtsPath, dts);
103
+ await fs.writeFile(localTsPath, ts);
104
+
105
+ projectInfo.log.info(`Client generated in ${Date.now() - now}ms`);
106
+
107
+ return { written: true, path: outDir };
108
+ }
@@ -0,0 +1,306 @@
1
+ import * as chokidar from 'chokidar';
2
+ import * as http from 'http';
3
+ import fs from 'fs/promises';
4
+ import getProjectInfo, { ProjectInfo } from '../getProjectInfo';
5
+ import path from 'path';
6
+ import { debouncedEnsureMetadataFiles } from './ensureMetadataFiles';
7
+ import createMetadataServer from './createMetadataServer';
8
+ import writeOneMetadataFile from './writeOneMetadataFile';
9
+ import logDiffResult from './logDiffResult';
10
+ import generateClient from './generateClient';
11
+ import locateSegments, { type Segment } from '../locateSegments';
12
+ import debounceWithArgs from '../utils/debounceWithArgs';
13
+ import { debounce, isEmpty } from 'lodash';
14
+ import { VovkEnv } from '../types';
15
+ import { VovkMetadata } from 'vovk';
16
+
17
+ export class VovkCLIServer {
18
+ #projectInfo: ProjectInfo;
19
+
20
+ #segments: Segment[] = [];
21
+
22
+ #metadata: Record<string, VovkMetadata> = {};
23
+
24
+ #isWatching = false;
25
+
26
+ #modulesWatcher: chokidar.FSWatcher | null = null;
27
+
28
+ #segmentWatcher: chokidar.FSWatcher | null = null;
29
+
30
+ #watchSegments = () => {
31
+ const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
32
+ const { apiDir, log, metadataOutFullPath } = this.#projectInfo;
33
+ const getSegmentName = (filePath: string) => path.relative(apiDir, filePath).replace(segmentReg, '');
34
+ log.debug(`Watching segments in ${apiDir}`);
35
+ this.#segmentWatcher = chokidar
36
+ .watch(apiDir, {
37
+ persistent: true,
38
+ ignoreInitial: true,
39
+ })
40
+ .on('add', (filePath) => {
41
+ log.debug(`File ${filePath} has been added to segments folder`);
42
+ if (segmentReg.test(filePath)) {
43
+ const segmentName = getSegmentName(filePath);
44
+
45
+ this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
46
+ ? this.#segments
47
+ : [...this.#segments, { routeFilePath: filePath, segmentName }];
48
+ log.info(`Segment "${segmentName}" has been added`);
49
+ log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
50
+
51
+ void debouncedEnsureMetadataFiles(
52
+ metadataOutFullPath,
53
+ this.#segments.map((s) => s.segmentName),
54
+ this.#projectInfo // TODO refactor
55
+ );
56
+ }
57
+ })
58
+ .on('change', (filePath) => {
59
+ log.debug(`File ${filePath} has been changed at segments folder`);
60
+ if (segmentReg.test(filePath)) {
61
+ void this.#ping(getSegmentName(filePath));
62
+ }
63
+ })
64
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
65
+ .on('addDir', async (dirPath) => {
66
+ log.debug(`Directory ${dirPath} has been added to segments folder`);
67
+ this.#segments = await locateSegments(this.#projectInfo.apiDir);
68
+ for (const { segmentName } of this.#segments) {
69
+ void this.#ping(segmentName);
70
+ }
71
+ })
72
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
73
+ .on('unlinkDir', async (dirPath) => {
74
+ log.debug(`Directory ${dirPath} has been removed from segments folder`);
75
+ this.#segments = await locateSegments(this.#projectInfo.apiDir);
76
+ for (const { segmentName } of this.#segments) {
77
+ void this.#ping(segmentName);
78
+ }
79
+ })
80
+ .on('unlink', (filePath) => {
81
+ log.debug(`File ${filePath} has been removed from segments folder`);
82
+ if (segmentReg.test(filePath)) {
83
+ const segmentName = getSegmentName(filePath);
84
+ this.#segments = this.#segments.filter((s) => s.segmentName !== segmentName);
85
+ log.info(`Segment "${segmentName}" has been removed`);
86
+ log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
87
+
88
+ void debouncedEnsureMetadataFiles(
89
+ metadataOutFullPath,
90
+ this.#segments.map((s) => s.segmentName),
91
+ this.#projectInfo // TODO refactor
92
+ );
93
+ }
94
+ })
95
+ .on('ready', () => {
96
+ log.debug('Segments watcher is ready');
97
+ })
98
+ .on('error', (error) => {
99
+ log.error(`Error watching segments folder: ${error.message}`);
100
+ });
101
+ };
102
+
103
+ #watchModules = () => {
104
+ const { config, log } = this.#projectInfo;
105
+ log.debug(`Watching modules in ${config.modulesDir}`);
106
+ this.#modulesWatcher = chokidar
107
+ .watch(config.modulesDir, {
108
+ persistent: true,
109
+ ignoreInitial: true,
110
+ })
111
+ .on('add', (filePath) => {
112
+ log.debug(`File ${filePath} has been added to modules folder`);
113
+ void this.#processControllerChange(filePath);
114
+ })
115
+ .on('change', (filePath) => {
116
+ log.debug(`File ${filePath} has been changed at modules folder`);
117
+ void this.#processControllerChange(filePath);
118
+ })
119
+ .on('unlink', (filePath) => {
120
+ log.debug(`File ${filePath} has been removed from modules folder`);
121
+ })
122
+ .on('addDir', () => {
123
+ for (const { segmentName } of this.#segments) {
124
+ void this.#ping(segmentName);
125
+ }
126
+ })
127
+ .on('unlinkDir', () => {
128
+ for (const { segmentName } of this.#segments) {
129
+ void this.#ping(segmentName);
130
+ }
131
+ })
132
+ .on('ready', () => {
133
+ log.debug('Modules watcher is ready');
134
+ })
135
+ .on('error', (error) => {
136
+ log.error(`Error watching modules folder: ${error.message}`);
137
+ });
138
+ };
139
+
140
+ #watchConfig = () => {
141
+ const { log } = this.#projectInfo;
142
+ log.debug(`Watching config files`);
143
+ let isInitial = true;
144
+ const handle = debounce(async () => {
145
+ this.#projectInfo = await getProjectInfo();
146
+ if (!isInitial) {
147
+ log.info('Config file has been updated');
148
+
149
+ isInitial = false;
150
+ }
151
+ await this.#modulesWatcher?.close();
152
+ await this.#segmentWatcher?.close();
153
+ this.#watchModules();
154
+ this.#watchSegments();
155
+ }, 1000);
156
+
157
+ chokidar
158
+ .watch('vovk.config.{js,mjs,cjs}', {
159
+ persistent: true,
160
+ cwd: process.cwd(),
161
+ ignoreInitial: false,
162
+ depth: 0,
163
+ })
164
+ .on('add', () => void handle())
165
+ .on('change', () => void handle())
166
+ .on('unlink', () => void handle())
167
+ .on('ready', () => {
168
+ log.debug('Config files watcher is ready');
169
+ })
170
+ .on('error', (error) => {
171
+ log.error(`Error watching config files: ${error.message}`);
172
+ });
173
+ };
174
+
175
+ #watch() {
176
+ if (this.#isWatching) throw new Error('Already watching');
177
+ const { apiDir, log, config } = this.#projectInfo;
178
+
179
+ log.info(
180
+ `Watching segments in ${apiDir} and modules in ${config.modulesDir}. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`
181
+ );
182
+
183
+ this.#watchSegments();
184
+ this.#watchModules();
185
+ this.#watchConfig();
186
+ }
187
+
188
+ #processControllerChange = async (filePath: string) => {
189
+ const { log } = this.#projectInfo;
190
+ const code = await fs.readFile(filePath, 'utf-8').catch(() => null);
191
+ if (typeof code !== 'string') return;
192
+ const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
193
+ const namesOfClasses = [...code.matchAll(nameOfClasReg)].map((match) => match[1]);
194
+
195
+ const importRegex =
196
+ /import\s*{[^}]*\b(initVovk|get|post|put|del|head|options|worker)\b[^}]*}\s*from\s*['"]vovk['"]/;
197
+ if (importRegex.test(code) && namesOfClasses.length) {
198
+ const affectedSegments = this.#segments.filter((s) => {
199
+ const metadata = this.#metadata[s.segmentName];
200
+ if (!metadata) return false;
201
+ return namesOfClasses.some((name) => metadata.controllers[name] || metadata.workers[name]);
202
+ });
203
+
204
+ if (affectedSegments.length) {
205
+ log.debug(
206
+ `A file with controller or worker ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${affectedSegments.map((s) => s.segmentName).join(', ')}`
207
+ );
208
+
209
+ for (const segment of affectedSegments) {
210
+ await this.#ping(segment.segmentName);
211
+ }
212
+ }
213
+ }
214
+ };
215
+
216
+ #ping = debounceWithArgs((segmentName: string) => {
217
+ const { apiEntryPoint, log } = this.#projectInfo;
218
+ const endpoint = `${apiEntryPoint}/${segmentName ? `${segmentName}/` : ''}_vovk-ping_`;
219
+
220
+ log.debug(`Pinging segment "${segmentName}" at ${endpoint}`);
221
+ const req = http.get(endpoint, (resp) => {
222
+ if (resp.statusCode !== 200) {
223
+ log.warn(`Ping to segment "${segmentName}" failed with status code ${resp.statusCode}. Expected 200.`);
224
+ }
225
+ });
226
+
227
+ req.on('error', (err) => {
228
+ log.error(`Error during HTTP request made to ${endpoint}: ${err.message}`);
229
+ });
230
+ }, 500);
231
+
232
+ #createMetadataServer() {
233
+ const { metadataOutFullPath, log } = this.#projectInfo;
234
+ return createMetadataServer(
235
+ async ({ metadata }) => {
236
+ const segment = this.#segments.find((s) => s.segmentName === metadata.segmentName);
237
+
238
+ if (!segment) {
239
+ log.debug(`Segment "${metadata.segmentName}" not found`);
240
+ return;
241
+ }
242
+
243
+ this.#metadata[metadata.segmentName] = metadata;
244
+ if (metadata.emitMetadata) {
245
+ const now = Date.now();
246
+ const { diffResult } = await writeOneMetadataFile({
247
+ metadataOutFullPath,
248
+ metadata,
249
+ skipIfExists: false,
250
+ });
251
+
252
+ const timeTook = Date.now() - now;
253
+
254
+ if (diffResult) {
255
+ logDiffResult(segment.segmentName, diffResult, this.#projectInfo);
256
+ log.info(`Metadata for segment "${metadata.segmentName}" has been updated in ${timeTook}ms`);
257
+ }
258
+ } else if (metadata && (!isEmpty(metadata.controllers) || !isEmpty(metadata.workers))) {
259
+ log.error(`Non-empty metadata provided for segment "${metadata.segmentName}" but emitMetadata is false`);
260
+ }
261
+
262
+ if (this.#segments.every((s) => this.#metadata[s.segmentName])) {
263
+ log.debug(`All segments with emitMetadata=true have metadata.`);
264
+ await generateClient(this.#projectInfo, this.#segments, this.#metadata);
265
+ }
266
+ },
267
+ (error) => {
268
+ log.error(String(error));
269
+ }
270
+ );
271
+ }
272
+
273
+ async startServer({ clientOutDir }: { clientOutDir?: string } = {}) {
274
+ this.#projectInfo = await getProjectInfo({ clientOutDir });
275
+ this.#segments = await locateSegments(this.#projectInfo.apiDir);
276
+ const { vovkPort, log, metadataOutFullPath } = this.#projectInfo;
277
+ const server = this.#createMetadataServer();
278
+
279
+ if (!vovkPort) {
280
+ log.error('No port provided for Vovk Server. Exiting...');
281
+ return;
282
+ }
283
+
284
+ await debouncedEnsureMetadataFiles(
285
+ metadataOutFullPath,
286
+ this.#segments.map((s) => s.segmentName),
287
+ this.#projectInfo
288
+ );
289
+
290
+ server.listen(vovkPort, () => {
291
+ log.info(`Vovk Metadata Server is running on port ${vovkPort}. Happy coding!`);
292
+ });
293
+
294
+ // Ping every segment in 3 seconds in order to update metadata and start watching
295
+ setTimeout(() => {
296
+ for (const { segmentName } of this.#segments) {
297
+ void this.#ping(segmentName);
298
+ }
299
+ this.#watch();
300
+ }, 3000);
301
+ }
302
+ }
303
+ const env = process.env as VovkEnv;
304
+ if (env.__VOVK_START_SERVER_IN_STANDALONE_MODE__ === 'true') {
305
+ void new VovkCLIServer().startServer();
306
+ }
@@ -0,0 +1,6 @@
1
+ import { isEmpty } from 'lodash';
2
+ import type { VovkMetadata } from 'vovk';
3
+
4
+ export default function isMetadataEmpty(metadata: VovkMetadata): boolean {
5
+ return isEmpty(metadata.controllers) && isEmpty(metadata.workers);
6
+ }
@@ -0,0 +1,114 @@
1
+ import chalk from 'chalk';
2
+ import { ProjectInfo } from '../getProjectInfo';
3
+ import { DiffResult } from './diffMetadata';
4
+
5
+ export default function logDiffResult(segmentName: string, diffResult: DiffResult, projectInfo: ProjectInfo) {
6
+ const diffNormalized: {
7
+ what: 'worker' | 'controller' | 'workerHandler' | 'controllerHandler';
8
+ type: 'added' | 'removed' | 'changed';
9
+ name: string;
10
+ }[] = [];
11
+
12
+ diffResult.workers.added.forEach((name) => {
13
+ diffNormalized.push({ what: 'worker', type: 'added', name });
14
+ });
15
+
16
+ diffResult.workers.removed.forEach((name) => {
17
+ diffNormalized.push({ what: 'worker', type: 'removed', name });
18
+ });
19
+
20
+ diffResult.controllers.added.forEach((name) => {
21
+ diffNormalized.push({ what: 'controller', type: 'added', name });
22
+ });
23
+
24
+ diffResult.controllers.removed.forEach((name) => {
25
+ diffNormalized.push({ what: 'controller', type: 'removed', name });
26
+ });
27
+
28
+ diffResult.controllers.handlers.forEach((handler) => {
29
+ handler.added.forEach((name) => {
30
+ diffNormalized.push({ what: 'controllerHandler', type: 'added', name: `${handler.nameOfClass}.${name}` });
31
+ });
32
+
33
+ handler.removed.forEach((name) => {
34
+ diffNormalized.push({ what: 'controllerHandler', type: 'removed', name: `${handler.nameOfClass}.${name}` });
35
+ });
36
+
37
+ handler.changed.forEach((name) => {
38
+ diffNormalized.push({ what: 'controllerHandler', type: 'changed', name: `${handler.nameOfClass}.${name}` });
39
+ });
40
+ });
41
+
42
+ const LIMIT = 5;
43
+
44
+ for (const diffNormalizedItem of diffNormalized.slice(0, LIMIT)) {
45
+ switch (diffNormalizedItem.what) {
46
+ case 'worker':
47
+ switch (diffNormalizedItem.type) {
48
+ case 'added':
49
+ projectInfo.log.info(
50
+ `New worker ${chalk.white.bold(diffNormalizedItem.name)} has been added to segment "${chalk.white.bold(segmentName)}"`
51
+ );
52
+ break;
53
+ case 'removed':
54
+ projectInfo.log.info(
55
+ `Worker ${chalk.white.bold(diffNormalizedItem.name)} has been removed from segment "${chalk.white.bold(segmentName)}"`
56
+ );
57
+ break;
58
+ }
59
+ break;
60
+ case 'controller':
61
+ switch (diffNormalizedItem.type) {
62
+ case 'added':
63
+ projectInfo.log.info(
64
+ `New controller ${chalk.white.bold(diffNormalizedItem.name)} has been added to segment "${chalk.white.bold(segmentName)}"`
65
+ );
66
+ break;
67
+ case 'removed':
68
+ projectInfo.log.info(
69
+ `Controller ${chalk.white.bold(diffNormalizedItem.name)} has been removed from segment "${chalk.white.bold(segmentName)}"`
70
+ );
71
+ break;
72
+ }
73
+ break;
74
+ case 'workerHandler':
75
+ switch (diffNormalizedItem.type) {
76
+ case 'added':
77
+ projectInfo.log.info(
78
+ `New worker method ${chalk.white.bold(diffNormalizedItem.name)} has been added in segment "${chalk.white.bold(segmentName)}"`
79
+ );
80
+ break;
81
+ case 'removed':
82
+ projectInfo.log.info(
83
+ `Worker method ${chalk.white.bold(diffNormalizedItem.name)} has been removed in segment "${chalk.white.bold(segmentName)}"`
84
+ );
85
+ break;
86
+ }
87
+ break;
88
+
89
+ case 'controllerHandler':
90
+ switch (diffNormalizedItem.type) {
91
+ case 'added':
92
+ projectInfo.log.info(
93
+ `New controller method ${chalk.white.bold(diffNormalizedItem.name)} has been added in segment "${chalk.white.bold(segmentName)}"`
94
+ );
95
+ break;
96
+ case 'removed':
97
+ projectInfo.log.info(
98
+ `Controller method ${chalk.white.bold(diffNormalizedItem.name)} has been removed in segment "${chalk.white.bold(segmentName)}"`
99
+ );
100
+ break;
101
+ case 'changed':
102
+ projectInfo.log.info(
103
+ `Controller method ${chalk.white.bold(diffNormalizedItem.name)} has been changed in segment "${chalk.white.bold(segmentName)}"`
104
+ );
105
+ break;
106
+ }
107
+ break;
108
+ }
109
+ }
110
+
111
+ if (diffNormalized.length > LIMIT) {
112
+ projectInfo.log.info(`...and ${diffNormalized.length - LIMIT} more changes`);
113
+ }
114
+ }
@@ -0,0 +1,44 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import { VovkMetadata } from 'vovk';
4
+ import diffMetadata, { DiffResult } from './diffMetadata';
5
+
6
+ export const ROOT_SEGMENT_SCHEMA_NAME = '_root';
7
+
8
+ export default async function writeOneMetadataFile({
9
+ metadataOutFullPath,
10
+ metadata,
11
+ skipIfExists = false,
12
+ }: {
13
+ metadataOutFullPath: string;
14
+ metadata: VovkMetadata;
15
+ skipIfExists?: boolean;
16
+ }): Promise<{
17
+ isCreated: boolean;
18
+ diffResult: DiffResult | null;
19
+ }> {
20
+ const segmentPath = path.join(metadataOutFullPath, `${metadata.segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json`);
21
+
22
+ if (skipIfExists) {
23
+ try {
24
+ await fs.stat(segmentPath);
25
+ return { isCreated: false, diffResult: null };
26
+ } catch {
27
+ // File doesn't exist
28
+ }
29
+ }
30
+
31
+ await fs.mkdir(path.dirname(segmentPath), { recursive: true });
32
+ const metadataStr = JSON.stringify(metadata, null, 2);
33
+ const existing = await fs.readFile(segmentPath, 'utf-8').catch(() => null);
34
+ if (existing === metadataStr) {
35
+ return { isCreated: false, diffResult: null };
36
+ }
37
+ await fs.writeFile(segmentPath, metadataStr);
38
+
39
+ if (existing) {
40
+ return { isCreated: false, diffResult: diffMetadata(JSON.parse(existing), metadata) };
41
+ }
42
+
43
+ return { isCreated: true, diffResult: null };
44
+ }