vovk-cli 0.0.1-draft.11 → 0.0.1-draft.112

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 (90) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -1
  3. package/client-templates/main/main.cjs.ejs +14 -0
  4. package/client-templates/main/main.d.cts.ejs +14 -0
  5. package/client-templates/module/module.d.mts.ejs +14 -0
  6. package/client-templates/module/module.mjs.ejs +20 -0
  7. package/client-templates/python/__init__.py +276 -0
  8. package/client-templates/ts/index.ts.ejs +23 -0
  9. package/dist/dev/diffSegmentSchema.d.mts +36 -0
  10. package/dist/{watcher/diffSchema.mjs → dev/diffSegmentSchema.mjs} +4 -12
  11. package/dist/{watcher → dev}/ensureSchemaFiles.d.mts +3 -0
  12. package/dist/{watcher → dev}/ensureSchemaFiles.mjs +53 -26
  13. package/dist/dev/index.d.mts +6 -0
  14. package/dist/{watcher → dev}/index.mjs +150 -77
  15. package/dist/dev/isSegmentSchemaEmpty.d.mts +2 -0
  16. package/dist/dev/isSegmentSchemaEmpty.mjs +4 -0
  17. package/dist/dev/logDiffResult.d.mts +3 -0
  18. package/dist/dev/logDiffResult.mjs +57 -0
  19. package/dist/dev/writeConfigJson.d.mts +2 -0
  20. package/dist/dev/writeConfigJson.mjs +15 -0
  21. package/dist/dev/writeOneSegmentSchemaFile.d.mts +12 -0
  22. package/dist/{watcher/writeOneSchemaFile.mjs → dev/writeOneSegmentSchemaFile.mjs} +11 -7
  23. package/dist/generate/ensureClient.d.mts +4 -0
  24. package/dist/generate/ensureClient.mjs +31 -0
  25. package/dist/generate/getClientTemplates.d.mts +16 -0
  26. package/dist/generate/getClientTemplates.mjs +42 -0
  27. package/dist/generate/index.d.mts +13 -0
  28. package/dist/generate/index.mjs +105 -0
  29. package/dist/getProjectInfo/getConfig.d.mts +3 -3
  30. package/dist/getProjectInfo/getConfig.mjs +22 -5
  31. package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +2 -2
  32. package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
  33. package/dist/getProjectInfo/getUserConfig.d.mts +1 -1
  34. package/dist/getProjectInfo/getUserConfig.mjs +3 -1
  35. package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
  36. package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
  37. package/dist/getProjectInfo/index.d.mts +14 -5
  38. package/dist/getProjectInfo/index.mjs +21 -13
  39. package/dist/index.d.mts +0 -28
  40. package/dist/index.mjs +60 -64
  41. package/dist/init/checkTSConfigForExperimentalDecorators.mjs +2 -2
  42. package/dist/init/createConfig.d.mts +3 -4
  43. package/dist/init/createConfig.mjs +12 -10
  44. package/dist/init/getTemplateFilesFromPackage.d.mts +2 -1
  45. package/dist/init/getTemplateFilesFromPackage.mjs +4 -5
  46. package/dist/init/index.d.mts +2 -3
  47. package/dist/init/index.mjs +31 -88
  48. package/dist/init/installDependencies.d.mts +1 -1
  49. package/dist/init/installDependencies.mjs +1 -1
  50. package/dist/init/logUpdateDependenciesError.d.mts +2 -2
  51. package/dist/init/logUpdateDependenciesError.mjs +3 -3
  52. package/dist/init/updateDependenciesWithoutInstalling.d.mts +3 -2
  53. package/dist/init/updateDependenciesWithoutInstalling.mjs +7 -9
  54. package/dist/init/updateNPMScripts.d.mts +3 -1
  55. package/dist/init/updateNPMScripts.mjs +13 -7
  56. package/dist/init/updateTypeScriptConfig.mjs +2 -2
  57. package/dist/initProgram.d.mts +2 -0
  58. package/dist/initProgram.mjs +21 -0
  59. package/dist/locateSegments.d.mts +7 -1
  60. package/dist/locateSegments.mjs +9 -6
  61. package/dist/new/addClassToSegmentCode.d.mts +1 -2
  62. package/dist/new/addClassToSegmentCode.mjs +5 -5
  63. package/dist/new/addCommonTerms.mjs +1 -0
  64. package/dist/new/index.d.mts +2 -2
  65. package/dist/new/index.mjs +3 -2
  66. package/dist/new/newModule.d.mts +3 -2
  67. package/dist/new/newModule.mjs +39 -34
  68. package/dist/new/newSegment.mjs +8 -6
  69. package/dist/new/render.d.mts +5 -2
  70. package/dist/new/render.mjs +25 -13
  71. package/dist/postinstall.mjs +16 -19
  72. package/dist/types.d.mts +35 -40
  73. package/dist/utils/debounceWithArgs.d.mts +2 -2
  74. package/dist/utils/debounceWithArgs.mjs +24 -9
  75. package/dist/utils/formatLoggedSegmentName.mjs +1 -1
  76. package/dist/utils/getAvailablePort.mjs +2 -1
  77. package/dist/utils/getFileSystemEntryType.mjs +1 -1
  78. package/package.json +23 -18
  79. package/templates/controller.ejs +22 -23
  80. package/templates/service.ejs +13 -13
  81. package/dist/generateClient.d.mts +0 -7
  82. package/dist/generateClient.mjs +0 -97
  83. package/dist/watcher/diffSchema.d.mts +0 -43
  84. package/dist/watcher/index.d.mts +0 -6
  85. package/dist/watcher/isMetadataEmpty.d.mts +0 -2
  86. package/dist/watcher/isMetadataEmpty.mjs +0 -4
  87. package/dist/watcher/logDiffResult.d.mts +0 -3
  88. package/dist/watcher/logDiffResult.mjs +0 -90
  89. package/dist/watcher/writeOneSchemaFile.d.mts +0 -11
  90. package/templates/worker.ejs +0 -24
@@ -1,33 +1,40 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
1
3
  import * as chokidar from 'chokidar';
2
- import fs from 'fs/promises';
3
- import getProjectInfo from '../getProjectInfo/index.mjs';
4
- import path from 'path';
4
+ import { Agent, setGlobalDispatcher } from 'undici';
5
+ import keyBy from 'lodash/keyBy.js';
6
+ import capitalize from 'lodash/capitalize.js';
7
+ import debounce from 'lodash/debounce.js';
8
+ import once from 'lodash/once.js';
5
9
  import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
6
- import writeOneSchemaFile from './writeOneSchemaFile.mjs';
10
+ import writeOneSegmentSchemaFile from './writeOneSegmentSchemaFile.mjs';
7
11
  import logDiffResult from './logDiffResult.mjs';
8
- import generateClient from '../generateClient.mjs';
12
+ import ensureClient from '../generate/ensureClient.mjs';
13
+ import getProjectInfo from '../getProjectInfo/index.mjs';
14
+ import generate from '../generate/index.mjs';
9
15
  import locateSegments from '../locateSegments.mjs';
10
16
  import debounceWithArgs from '../utils/debounceWithArgs.mjs';
11
- import debounce from 'lodash/debounce.js';
12
- import isEmpty from 'lodash/isEmpty.js';
13
17
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
14
- import keyBy from 'lodash/keyBy.js';
15
- import capitalize from 'lodash/capitalize.js';
16
- import { Agent, setGlobalDispatcher } from 'undici';
17
- export class VovkCLIWatcher {
18
+ import isSegmentSchemaEmpty from './isSegmentSchemaEmpty.mjs';
19
+ import writeConfigJson from './writeConfigJson.mjs';
20
+ export class VovkDev {
18
21
  #projectInfo;
19
22
  #segments = [];
20
- #schemas = {};
23
+ #fullSchema = {
24
+ segments: {},
25
+ config: {},
26
+ };
21
27
  #isWatching = false;
22
28
  #modulesWatcher = null;
23
29
  #segmentWatcher = null;
24
- #watchSegments = () => {
30
+ #onFirstTimeGenerate = null;
31
+ #watchSegments = (callback) => {
25
32
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
26
33
  const { cwd, log, config, apiDir } = this.#projectInfo;
27
34
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
28
35
  const apiDirAbsolutePath = path.join(cwd, apiDir);
29
36
  const getSegmentName = (filePath) => path.relative(apiDirAbsolutePath, filePath).replace(segmentReg, '');
30
- log.debug(`Watching segments in ${apiDirAbsolutePath}`);
37
+ log.debug(`Watching segments at ${apiDirAbsolutePath}`);
31
38
  this.#segmentWatcher = chokidar
32
39
  .watch(apiDirAbsolutePath, {
33
40
  persistent: true,
@@ -39,7 +46,14 @@ export class VovkCLIWatcher {
39
46
  const segmentName = getSegmentName(filePath);
40
47
  this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
41
48
  ? this.#segments
42
- : [...this.#segments, { routeFilePath: filePath, segmentName }];
49
+ : [
50
+ ...this.#segments,
51
+ {
52
+ routeFilePath: filePath,
53
+ segmentName,
54
+ segmentImportPath: path.relative(config.clientOutDir, filePath), // TODO DRY locateSegments
55
+ },
56
+ ];
43
57
  log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
44
58
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
45
59
  void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
@@ -53,14 +67,14 @@ export class VovkCLIWatcher {
53
67
  })
54
68
  .on('addDir', async (dirPath) => {
55
69
  log.debug(`Directory ${dirPath} has been added to segments folder`);
56
- this.#segments = await locateSegments(apiDirAbsolutePath);
70
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
57
71
  for (const { segmentName } of this.#segments) {
58
72
  void this.#requestSchema(segmentName);
59
73
  }
60
74
  })
61
75
  .on('unlinkDir', async (dirPath) => {
62
76
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
63
- this.#segments = await locateSegments(apiDirAbsolutePath);
77
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
64
78
  for (const { segmentName } of this.#segments) {
65
79
  void this.#requestSchema(segmentName);
66
80
  }
@@ -76,16 +90,17 @@ export class VovkCLIWatcher {
76
90
  }
77
91
  })
78
92
  .on('ready', () => {
93
+ callback();
79
94
  log.debug('Segments watcher is ready');
80
95
  })
81
96
  .on('error', (error) => {
82
97
  log.error(`Error watching segments folder: ${error?.message ?? 'Unknown error'}`);
83
98
  });
84
99
  };
85
- #watchModules = () => {
100
+ #watchModules = (callback) => {
86
101
  const { config, cwd, log } = this.#projectInfo;
87
102
  const modulesDirAbsolutePath = path.join(cwd, config.modulesDir);
88
- log.debug(`Watching modules in ${modulesDirAbsolutePath}`);
103
+ log.debug(`Watching modules at ${modulesDirAbsolutePath}`);
89
104
  const processControllerChange = debounceWithArgs(this.#processControllerChange, 500);
90
105
  this.#modulesWatcher = chokidar
91
106
  .watch(modulesDirAbsolutePath, {
@@ -114,30 +129,47 @@ export class VovkCLIWatcher {
114
129
  }
115
130
  })
116
131
  .on('ready', () => {
132
+ callback();
117
133
  log.debug('Modules watcher is ready');
118
134
  })
119
135
  .on('error', (error) => {
120
136
  log.error(`Error watching modules folder: ${error?.message ?? 'Unknown error'}`);
121
137
  });
122
138
  };
123
- #watchConfig = () => {
139
+ #watchConfig = (callback) => {
124
140
  const { log, cwd } = this.#projectInfo;
125
141
  log.debug(`Watching config files`);
126
142
  let isInitial = true;
127
143
  let isReady = false;
128
144
  const handle = debounce(async () => {
129
145
  this.#projectInfo = await getProjectInfo();
130
- if (!isInitial) {
131
- log.info('Config file has been updated');
132
- isInitial = false;
133
- }
134
146
  await this.#modulesWatcher?.close();
135
147
  await this.#segmentWatcher?.close();
136
- this.#watchModules();
137
- this.#watchSegments();
148
+ await Promise.all([
149
+ new Promise((resolve) => this.#watchModules(() => resolve(0))),
150
+ new Promise((resolve) => this.#watchSegments(() => resolve(0))),
151
+ ]);
152
+ const schemaOutAbsolutePath = path.join(cwd, this.#projectInfo.config.schemaOutDir);
153
+ if (isInitial) {
154
+ callback();
155
+ }
156
+ else {
157
+ log.info('Config file has been updated');
158
+ this.#generate();
159
+ }
160
+ await writeConfigJson(schemaOutAbsolutePath, this.#projectInfo);
161
+ isInitial = false;
138
162
  }, 1000);
139
163
  chokidar
140
- .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
164
+ // .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
165
+ .watch([
166
+ 'vovk.config.js',
167
+ 'vovk.config.mjs',
168
+ 'vovk.config.cjs',
169
+ '.config/vovk.config.js',
170
+ '.config/vovk.config.mjs',
171
+ '.config/vovk.config.cjs',
172
+ ], {
141
173
  persistent: true,
142
174
  cwd,
143
175
  ignoreInitial: false,
@@ -156,13 +188,14 @@ export class VovkCLIWatcher {
156
188
  .on('error', (error) => log.error(`Error watching config files: ${error?.message ?? 'Unknown error'}`));
157
189
  void handle();
158
190
  };
159
- #watch() {
191
+ async #watch(callback) {
160
192
  if (this.#isWatching)
161
193
  throw new Error('Already watching');
162
194
  const { log } = this.#projectInfo;
163
195
  log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
196
+ await ensureClient(this.#projectInfo);
164
197
  // automatically watches segments and modules
165
- this.#watchConfig();
198
+ this.#watchConfig(callback);
166
199
  }
167
200
  #processControllerChange = async (filePath) => {
168
201
  const { log } = this.#projectInfo;
@@ -173,72 +206,78 @@ export class VovkCLIWatcher {
173
206
  }
174
207
  const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
175
208
  const namesOfClasses = [...code.matchAll(nameOfClasReg)].map((match) => match[1]);
176
- const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options|worker)\b[^}]*}\s*from\s*['"]vovk['"]/;
209
+ const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
177
210
  if (importRegex.test(code) && namesOfClasses.length) {
178
211
  const affectedSegments = this.#segments.filter((s) => {
179
- const schema = this.#schemas[s.segmentName];
180
- if (!schema)
212
+ const segmentSchema = this.#fullSchema.segments[s.segmentName];
213
+ if (!segmentSchema)
181
214
  return false;
182
- const controllersByOriginalName = keyBy(schema.controllers, '_originalControllerName');
183
- const workersByOriginalName = keyBy(schema.workers, '_originalWorkerName');
184
- return namesOfClasses.some((name) => schema.controllers[name] ||
185
- schema.workers[name] ||
186
- controllersByOriginalName[name] ||
187
- workersByOriginalName[name]);
215
+ const controllersByOriginalName = keyBy(segmentSchema.controllers, 'originalControllerName');
216
+ return namesOfClasses.some((name) => segmentSchema.controllers[name] || controllersByOriginalName[name]);
188
217
  });
189
218
  if (affectedSegments.length) {
190
- log.debug(`A file with controller or worker ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${JSON.stringify(affectedSegments.map((s) => s.segmentName))}`);
219
+ log.debug(`A file with controller ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${JSON.stringify(affectedSegments.map((s) => s.segmentName))}`);
191
220
  for (const segment of affectedSegments) {
192
221
  await this.#requestSchema(segment.segmentName);
193
222
  }
194
223
  }
224
+ else {
225
+ log.debug(`The class ${namesOfClasses.join(', ')} does not belong to any segment`);
226
+ }
195
227
  }
196
228
  else {
197
- log.debug(`The file does not contain any controller or worker`);
229
+ log.debug(`The file does not contain any controller`);
198
230
  }
199
231
  };
200
232
  #requestSchema = debounceWithArgs(async (segmentName) => {
201
- const { apiEntryPoint, log, port, config } = this.#projectInfo;
233
+ const { apiRoot, log, port, config } = this.#projectInfo;
202
234
  const { devHttps } = config;
203
- const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
235
+ const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
204
236
  log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
205
- const resp = await fetch(endpoint);
206
- if (resp.status !== 200) {
207
- const probableCause = {
208
- 404: 'The segment is not compiled.',
209
- 500: 'Syntax error in one of controllers.',
210
- }[resp.status];
211
- log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
212
- return;
213
- }
214
- let schema = null;
215
237
  try {
216
- ({ schema } = (await resp.json()));
238
+ const resp = await fetch(endpoint);
239
+ if (resp.status !== 200) {
240
+ const probableCause = {
241
+ 404: 'The segment did not compile or config.origin is wrong.',
242
+ }[resp.status];
243
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
244
+ return { isError: true };
245
+ }
246
+ let segmentSchema = null;
247
+ try {
248
+ ({ schema: segmentSchema } = (await resp.json()));
249
+ }
250
+ catch (error) {
251
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
252
+ }
253
+ await this.#handleSegmentSchema(segmentName, segmentSchema);
217
254
  }
218
255
  catch (error) {
219
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
256
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
257
+ return { isError: true };
220
258
  }
221
- await this.#handleSchema(schema);
259
+ return { isError: false };
222
260
  }, 500);
223
- async #handleSchema(schema) {
261
+ #generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, fullSchema: this.#fullSchema }).then(this.#onFirstTimeGenerate), 1000);
262
+ async #handleSegmentSchema(segmentName, segmentSchema) {
224
263
  const { log, config, cwd } = this.#projectInfo;
225
- if (!schema) {
226
- log.warn('Segment schema is null');
264
+ if (!segmentSchema) {
265
+ log.warn(`${formatLoggedSegmentName(segmentName)} schema is null`);
227
266
  return;
228
267
  }
229
- log.debug(`Handling received schema from ${formatLoggedSegmentName(schema.segmentName)}`);
268
+ log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
230
269
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
231
- const segment = this.#segments.find((s) => s.segmentName === schema.segmentName);
270
+ const segment = this.#segments.find((s) => s.segmentName === segmentName);
232
271
  if (!segment) {
233
- log.warn(`Segment "${schema.segmentName}" not found`);
272
+ log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
234
273
  return;
235
274
  }
236
- this.#schemas[schema.segmentName] = schema;
237
- if (schema.emitSchema) {
275
+ this.#fullSchema.segments[segmentName] = segmentSchema;
276
+ if (segmentSchema.emitSchema) {
238
277
  const now = Date.now();
239
- const { diffResult } = await writeOneSchemaFile({
278
+ const { diffResult } = await writeOneSegmentSchemaFile({
240
279
  schemaOutAbsolutePath,
241
- schema,
280
+ segmentSchema,
242
281
  skipIfExists: false,
243
282
  });
244
283
  const timeTook = Date.now() - now;
@@ -247,17 +286,24 @@ export class VovkCLIWatcher {
247
286
  log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
248
287
  }
249
288
  }
250
- else if (schema && (!isEmpty(schema.controllers) || !isEmpty(schema.workers))) {
251
- log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but emitSchema is false`);
289
+ else if (segmentSchema && !isSegmentSchemaEmpty(segmentSchema)) {
290
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
252
291
  }
253
- if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
292
+ if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
254
293
  log.debug(`All segments with "emitSchema" have schema.`);
255
- await generateClient(this.#projectInfo, this.#segments, this.#schemas);
294
+ this.#generate();
256
295
  }
257
296
  }
258
- async start({ clientOutDir } = {}) {
259
- this.#projectInfo = await getProjectInfo({ clientOutDir });
297
+ async start({ exit }) {
298
+ const now = Date.now();
299
+ this.#projectInfo = await getProjectInfo();
260
300
  const { log, config, cwd, apiDir } = this.#projectInfo;
301
+ log.info('Starting...');
302
+ if (exit) {
303
+ this.#onFirstTimeGenerate = once(() => {
304
+ log.info('The schemas and the RPC client have been generated. Exiting...');
305
+ });
306
+ }
261
307
  if (config.devHttps) {
262
308
  const agent = new Agent({
263
309
  connect: {
@@ -274,18 +320,45 @@ export class VovkCLIWatcher {
274
320
  });
275
321
  const apiDirAbsolutePath = path.join(cwd, apiDir);
276
322
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
277
- this.#segments = await locateSegments(apiDirAbsolutePath);
323
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
278
324
  await debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
279
- // Request schema every segment in 5 seconds in order to update schema and start watching
325
+ const MAX_ATTEMPTS = 5;
326
+ const DELAY = 5000;
327
+ // Request schema every segment in 5 seconds in order to update schema on start
280
328
  setTimeout(() => {
281
329
  for (const { segmentName } of this.#segments) {
282
- void this.#requestSchema(segmentName);
330
+ let attempts = 0;
331
+ void this.#requestSchema(segmentName).then(({ isError }) => {
332
+ if (isError) {
333
+ const interval = setInterval(() => {
334
+ attempts++;
335
+ if (attempts >= MAX_ATTEMPTS) {
336
+ clearInterval(interval);
337
+ log.error(`Failed to request schema for ${formatLoggedSegmentName(segmentName)} after ${MAX_ATTEMPTS} attempts`);
338
+ return;
339
+ }
340
+ void this.#requestSchema(segmentName).then(({ isError: isError2 }) => {
341
+ if (!isError2) {
342
+ clearInterval(interval);
343
+ log.info(`Requested schema for ${formatLoggedSegmentName(segmentName)} after ${attempts} attempts`);
344
+ }
345
+ });
346
+ }, DELAY);
347
+ }
348
+ });
283
349
  }
284
- this.#watch();
285
- }, 5000);
350
+ }, DELAY);
351
+ if (!exit) {
352
+ this.#watch(() => {
353
+ log.info(`Ready in ${Date.now() - now}ms. Making initial requests for schemas in a moment...`);
354
+ });
355
+ }
356
+ else {
357
+ log.info(`Ready in ${Date.now() - now}ms. Making requests for schemas in a moment...`);
358
+ }
286
359
  }
287
360
  }
288
361
  const env = process.env;
289
362
  if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
290
- void new VovkCLIWatcher().start();
363
+ void new VovkDev().start({ exit: env.__VOVK_EXIT__ === 'true' });
291
364
  }
@@ -0,0 +1,2 @@
1
+ import type { VovkSegmentSchema } from 'vovk';
2
+ export default function isSegmentSchemaEmpty(segmentSchema: VovkSegmentSchema): boolean;
@@ -0,0 +1,4 @@
1
+ import isEmpty from 'lodash/isEmpty.js';
2
+ export default function isSegmentSchemaEmpty(segmentSchema) {
3
+ return isEmpty(segmentSchema.controllers);
4
+ }
@@ -0,0 +1,3 @@
1
+ import type { DiffResult } from './diffSegmentSchema.mjs';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ export default function logDiffResult(segmentName: string, diffResult: DiffResult, projectInfo: ProjectInfo): void;
@@ -0,0 +1,57 @@
1
+ import chalk from 'chalk';
2
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
3
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
4
+ export default function logDiffResult(segmentName, diffResult, projectInfo) {
5
+ const diffNormalized = [];
6
+ diffResult.controllers.added.forEach((name) => {
7
+ diffNormalized.push({ what: 'controller', type: 'added', name });
8
+ });
9
+ diffResult.controllers.removed.forEach((name) => {
10
+ diffNormalized.push({ what: 'controller', type: 'removed', name });
11
+ });
12
+ diffResult.controllers.handlers.forEach((handler) => {
13
+ handler.added.forEach((name) => {
14
+ diffNormalized.push({ what: 'controllerHandler', type: 'added', name: `${handler.nameOfClass}.${name}` });
15
+ });
16
+ handler.removed.forEach((name) => {
17
+ diffNormalized.push({ what: 'controllerHandler', type: 'removed', name: `${handler.nameOfClass}.${name}` });
18
+ });
19
+ handler.changed.forEach((name) => {
20
+ diffNormalized.push({ what: 'controllerHandler', type: 'changed', name: `${handler.nameOfClass}.${name}` });
21
+ });
22
+ });
23
+ const LIMIT = diffNormalized.length < 17 ? diffNormalized.length : 15;
24
+ const addedText = chalk.green('added');
25
+ const removedText = chalk.red('removed');
26
+ const changedText = chalk.cyan('changed');
27
+ for (const diffNormalizedItem of diffNormalized.slice(0, LIMIT)) {
28
+ switch (diffNormalizedItem.what) {
29
+ case 'controller':
30
+ switch (diffNormalizedItem.type) {
31
+ case 'added':
32
+ projectInfo.log.info(`Schema for RPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
33
+ break;
34
+ case 'removed':
35
+ projectInfo.log.info(`Schema for RPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
36
+ break;
37
+ }
38
+ break;
39
+ case 'controllerHandler':
40
+ switch (diffNormalizedItem.type) {
41
+ case 'added':
42
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
43
+ break;
44
+ case 'removed':
45
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
46
+ break;
47
+ case 'changed':
48
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${changedText} at ${formatLoggedSegmentName(segmentName)}`);
49
+ break;
50
+ }
51
+ break;
52
+ }
53
+ }
54
+ if (diffNormalized.length > LIMIT) {
55
+ projectInfo.log.info(`... and ${diffNormalized.length - LIMIT} more changes`);
56
+ }
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function writeConfigJson(schemaOutAbsolutePath: string, projectInfo: ProjectInfo | null): Promise<void>;
@@ -0,0 +1,15 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import pick from 'lodash/pick.js';
4
+ export default async function writeConfigJson(schemaOutAbsolutePath, projectInfo) {
5
+ const configJsonPath = path.join(schemaOutAbsolutePath, 'config.json');
6
+ const configStr = JSON.stringify(projectInfo ? pick(projectInfo.config, projectInfo.config.emitConfig) : {}, null, 2);
7
+ const existingStr = await fs.readFile(configJsonPath, 'utf-8').catch(() => null);
8
+ if (existingStr !== configStr) {
9
+ await fs.writeFile(configJsonPath, configStr);
10
+ projectInfo?.log.info(`Config JSON written to ${configJsonPath}`);
11
+ }
12
+ else {
13
+ projectInfo?.log.debug(`Config JSON is up to date at ${configJsonPath}`);
14
+ }
15
+ }
@@ -0,0 +1,12 @@
1
+ import type { VovkSegmentSchema } from 'vovk';
2
+ import { type DiffResult } from './diffSegmentSchema.mjs';
3
+ export declare const ROOT_SEGMENT_SCHEMA_NAME = "_root";
4
+ export declare const SEGMENTS_SCHEMA_DIR_NAME = "segments";
5
+ export default function writeOneSegmentSchemaFile({ schemaOutAbsolutePath, segmentSchema, skipIfExists, }: {
6
+ schemaOutAbsolutePath: string;
7
+ segmentSchema: VovkSegmentSchema;
8
+ skipIfExists?: boolean;
9
+ }): Promise<{
10
+ isCreated: boolean;
11
+ diffResult: DiffResult | null;
12
+ }>;
@@ -1,10 +1,11 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
- import diffSchema from './diffSchema.mjs';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import diffSegmentSchema from './diffSegmentSchema.mjs';
4
4
  import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
5
5
  export const ROOT_SEGMENT_SCHEMA_NAME = '_root';
6
- export default async function writeOneSchemaFile({ schemaOutAbsolutePath, schema, skipIfExists = false, }) {
7
- const segmentPath = path.join(schemaOutAbsolutePath, `${schema.segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json`);
6
+ export const SEGMENTS_SCHEMA_DIR_NAME = 'segments';
7
+ export default async function writeOneSegmentSchemaFile({ schemaOutAbsolutePath, segmentSchema, skipIfExists = false, }) {
8
+ const segmentPath = path.join(schemaOutAbsolutePath, SEGMENTS_SCHEMA_DIR_NAME, `${segmentSchema.segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json`);
8
9
  if (skipIfExists && (await getFileSystemEntryType(segmentPath))) {
9
10
  try {
10
11
  await fs.stat(segmentPath);
@@ -15,14 +16,17 @@ export default async function writeOneSchemaFile({ schemaOutAbsolutePath, schema
15
16
  }
16
17
  }
17
18
  await fs.mkdir(path.dirname(segmentPath), { recursive: true });
18
- const schemaStr = JSON.stringify(schema, null, 2);
19
+ const schemaStr = JSON.stringify(segmentSchema, null, 2);
19
20
  const existing = await fs.readFile(segmentPath, 'utf-8').catch(() => null);
20
21
  if (existing === schemaStr) {
21
22
  return { isCreated: false, diffResult: null };
22
23
  }
23
24
  await fs.writeFile(segmentPath, schemaStr);
24
25
  if (existing) {
25
- return { isCreated: false, diffResult: diffSchema(JSON.parse(existing), schema) };
26
+ return {
27
+ isCreated: false,
28
+ diffResult: diffSegmentSchema(JSON.parse(existing), segmentSchema),
29
+ };
26
30
  }
27
31
  return { isCreated: true, diffResult: null };
28
32
  }
@@ -0,0 +1,4 @@
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function ensureClient({ config, cwd, log }: ProjectInfo): Promise<{
3
+ written: boolean;
4
+ }>;
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs/promises';
2
+ import getClientTemplates from './getClientTemplates.mjs';
3
+ import uniq from 'lodash/uniq.js';
4
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
5
+ import path from 'node:path';
6
+ export default async function ensureClient({ config, cwd, log }) {
7
+ const now = Date.now();
8
+ const { clientOutDirAbsolutePath, templateFiles } = getClientTemplates({
9
+ config,
10
+ cwd,
11
+ generateFrom: config.generateFrom,
12
+ });
13
+ let usedTemplateNames = [];
14
+ const text = `// auto-generated ${new Date().toISOString()}
15
+ // This is a temporary placeholder to avoid compilation errors if client is imported before it's generated.
16
+ // If you still see this text, the client is not generated yet because of an unknown problem.
17
+ // Feel free to report an issue at https://github.com/finom/vovk/issues`;
18
+ for (const { outPath, templateName } of templateFiles) {
19
+ const existing = await fs.readFile(outPath, 'utf-8').catch(() => null);
20
+ if (!existing) {
21
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
22
+ await fs.writeFile(outPath, outPath.endsWith('.py') ? text.replace(/\/\//g, '#') : text);
23
+ usedTemplateNames.push(templateName);
24
+ }
25
+ }
26
+ usedTemplateNames = uniq(usedTemplateNames);
27
+ if (usedTemplateNames.length) {
28
+ log.info(`Placeholder client files from template${usedTemplateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(usedTemplateNames.map((s) => `"${s}"`).join(', '))} are generated at ${clientOutDirAbsolutePath} in ${Date.now() - now}ms`);
29
+ }
30
+ return { written: !!usedTemplateNames.length };
31
+ }
@@ -0,0 +1,16 @@
1
+ import type { VovkStrictConfig } from 'vovk';
2
+ interface ClientTemplate {
3
+ templateName: string;
4
+ templatePath: string;
5
+ outPath: string;
6
+ emitFullSchema?: string | boolean;
7
+ }
8
+ export default function getClientTemplates({ config, cwd, generateFrom, }: {
9
+ config: VovkStrictConfig;
10
+ cwd: string;
11
+ generateFrom?: VovkStrictConfig['generateFrom'];
12
+ }): {
13
+ clientOutDirAbsolutePath: string;
14
+ templateFiles: ClientTemplate[];
15
+ };
16
+ export {};
@@ -0,0 +1,42 @@
1
+ import path from 'node:path';
2
+ export default function getClientTemplates({ config, cwd, generateFrom = [], }) {
3
+ const templatesDir = path.join(import.meta.dirname, '../..', 'client-templates');
4
+ const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
5
+ const mapper = (dir) => (name) => ({
6
+ templateName: dir,
7
+ templatePath: path.resolve(templatesDir, dir, name),
8
+ outPath: path.join(clientOutDirAbsolutePath, name.replace('.ejs', '')),
9
+ });
10
+ const builtInTemplatesMap = {
11
+ ts: ['index.ts.ejs'].map(mapper('ts')),
12
+ main: ['main.cjs.ejs', 'main.d.cts.ejs'].map(mapper('main')),
13
+ module: ['module.mjs.ejs', 'module.d.mts.ejs'].map(mapper('module')),
14
+ python: ['__init__.py'].map(mapper('python')),
15
+ };
16
+ const templateFiles = (generateFrom ?? config.generateFrom).reduce((acc, template) => {
17
+ if (typeof template === 'string') {
18
+ if (template in builtInTemplatesMap) {
19
+ return [...acc, ...builtInTemplatesMap[template]];
20
+ }
21
+ return [
22
+ ...acc,
23
+ {
24
+ templateName: template,
25
+ templatePath: path.resolve(cwd, template),
26
+ outPath: path.join(clientOutDirAbsolutePath, path.basename(template).replace('.ejs', '')),
27
+ },
28
+ ];
29
+ }
30
+ const outDirAbsolutePath = template.outDir ? path.resolve(cwd, template.outDir) : clientOutDirAbsolutePath;
31
+ return [
32
+ ...acc,
33
+ {
34
+ templateName: template.templateName ?? template.templatePath,
35
+ templatePath: path.resolve(cwd, template.templatePath),
36
+ outPath: path.join(outDirAbsolutePath, path.basename(template.templatePath).replace('.ejs', '')),
37
+ fullSchema: template.fullSchema,
38
+ },
39
+ ];
40
+ }, []);
41
+ return { clientOutDirAbsolutePath, templateFiles };
42
+ }
@@ -0,0 +1,13 @@
1
+ import type { VovkFullSchema } from 'vovk';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ import type { Segment } from '../locateSegments.mjs';
4
+ import type { GenerateOptions } from '../types.mjs';
5
+ export default function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }: {
6
+ projectInfo: ProjectInfo;
7
+ segments: Segment[];
8
+ forceNothingWrittenLog?: boolean;
9
+ fullSchema: VovkFullSchema;
10
+ } & Pick<GenerateOptions, 'templates' | 'prettify' | 'emitFullSchema'>): Promise<{
11
+ written: boolean;
12
+ path: string;
13
+ }>;