vovk-cli 0.0.1-draft.12 → 0.0.1-draft.121

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 (97) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -1
  3. package/client-templates/fullSchema/fullSchema.cjs.ejs +13 -0
  4. package/client-templates/fullSchema/fullSchema.d.cts.ejs +11 -0
  5. package/client-templates/main/main.cjs.ejs +15 -0
  6. package/client-templates/main/main.d.cts.ejs +15 -0
  7. package/client-templates/module/module.d.mts.ejs +15 -0
  8. package/client-templates/module/module.mjs.ejs +21 -0
  9. package/client-templates/ts/index.ts.ejs +24 -0
  10. package/dist/dev/diffSegmentSchema.d.mts +36 -0
  11. package/dist/{watcher/diffSchema.mjs → dev/diffSegmentSchema.mjs} +4 -12
  12. package/dist/{watcher → dev}/ensureSchemaFiles.d.mts +3 -0
  13. package/dist/{watcher → dev}/ensureSchemaFiles.mjs +14 -32
  14. package/dist/dev/index.d.mts +6 -0
  15. package/dist/{watcher → dev}/index.mjs +152 -79
  16. package/dist/dev/isSegmentSchemaEmpty.d.mts +2 -0
  17. package/dist/dev/isSegmentSchemaEmpty.mjs +4 -0
  18. package/dist/dev/logDiffResult.d.mts +3 -0
  19. package/dist/dev/logDiffResult.mjs +57 -0
  20. package/dist/dev/writeConfigJson.d.mts +2 -0
  21. package/dist/dev/writeConfigJson.mjs +15 -0
  22. package/dist/dev/writeOneSegmentSchemaFile.d.mts +12 -0
  23. package/dist/{watcher/writeOneSchemaFile.mjs → dev/writeOneSegmentSchemaFile.mjs} +11 -7
  24. package/dist/generate/ensureClient.d.mts +4 -0
  25. package/dist/generate/ensureClient.mjs +41 -0
  26. package/dist/generate/getClientTemplates.d.mts +24 -0
  27. package/dist/generate/getClientTemplates.mjs +86 -0
  28. package/dist/generate/getFullSchemaFromJSON.d.mts +3 -0
  29. package/dist/generate/getFullSchemaFromJSON.mjs +64 -0
  30. package/dist/generate/index.d.mts +13 -0
  31. package/dist/generate/index.mjs +115 -0
  32. package/dist/getProjectInfo/getConfig.d.mts +5 -4
  33. package/dist/getProjectInfo/getConfig.mjs +26 -7
  34. package/dist/getProjectInfo/getConfigAbsolutePaths.d.mts +2 -1
  35. package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +6 -3
  36. package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
  37. package/dist/getProjectInfo/getUserConfig.d.mts +3 -2
  38. package/dist/getProjectInfo/getUserConfig.mjs +5 -3
  39. package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
  40. package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
  41. package/dist/getProjectInfo/index.d.mts +14 -6
  42. package/dist/getProjectInfo/index.mjs +23 -15
  43. package/dist/index.d.mts +0 -28
  44. package/dist/index.mjs +60 -64
  45. package/dist/init/checkTSConfigForExperimentalDecorators.mjs +2 -2
  46. package/dist/init/createConfig.d.mts +3 -4
  47. package/dist/init/createConfig.mjs +14 -10
  48. package/dist/init/getTemplateFilesFromPackage.d.mts +2 -1
  49. package/dist/init/getTemplateFilesFromPackage.mjs +4 -5
  50. package/dist/init/index.d.mts +2 -3
  51. package/dist/init/index.mjs +31 -88
  52. package/dist/init/installDependencies.d.mts +1 -1
  53. package/dist/init/installDependencies.mjs +1 -1
  54. package/dist/init/logUpdateDependenciesError.d.mts +2 -2
  55. package/dist/init/logUpdateDependenciesError.mjs +3 -3
  56. package/dist/init/updateDependenciesWithoutInstalling.d.mts +3 -2
  57. package/dist/init/updateDependenciesWithoutInstalling.mjs +7 -9
  58. package/dist/init/updateNPMScripts.d.mts +3 -1
  59. package/dist/init/updateNPMScripts.mjs +10 -7
  60. package/dist/init/updateTypeScriptConfig.mjs +2 -2
  61. package/dist/initProgram.d.mts +2 -0
  62. package/dist/initProgram.mjs +21 -0
  63. package/dist/locateSegments.d.mts +7 -1
  64. package/dist/locateSegments.mjs +9 -6
  65. package/dist/new/addClassToSegmentCode.d.mts +1 -2
  66. package/dist/new/addClassToSegmentCode.mjs +5 -5
  67. package/dist/new/addCommonTerms.mjs +1 -0
  68. package/dist/new/index.d.mts +2 -2
  69. package/dist/new/index.mjs +3 -2
  70. package/dist/new/newModule.d.mts +3 -2
  71. package/dist/new/newModule.mjs +41 -37
  72. package/dist/new/newSegment.mjs +8 -6
  73. package/dist/new/render.d.mts +6 -3
  74. package/dist/new/render.mjs +25 -13
  75. package/dist/types.d.mts +36 -40
  76. package/dist/utils/debounceWithArgs.d.mts +2 -2
  77. package/dist/utils/debounceWithArgs.mjs +24 -9
  78. package/dist/utils/formatLoggedSegmentName.mjs +1 -1
  79. package/dist/utils/getAvailablePort.mjs +2 -1
  80. package/dist/utils/getFileSystemEntryType.mjs +1 -1
  81. package/dist/utils/resolveAbsoluteModulePath.d.mts +1 -0
  82. package/dist/utils/resolveAbsoluteModulePath.mjs +6 -0
  83. package/package.json +26 -21
  84. package/templates/controller.ejs +22 -23
  85. package/templates/service.ejs +13 -13
  86. package/dist/generateClient.d.mts +0 -7
  87. package/dist/generateClient.mjs +0 -97
  88. package/dist/postinstall.d.mts +0 -1
  89. package/dist/postinstall.mjs +0 -24
  90. package/dist/watcher/diffSchema.d.mts +0 -43
  91. package/dist/watcher/index.d.mts +0 -6
  92. package/dist/watcher/isMetadataEmpty.d.mts +0 -2
  93. package/dist/watcher/isMetadataEmpty.mjs +0 -4
  94. package/dist/watcher/logDiffResult.d.mts +0 -3
  95. package/dist/watcher/logDiffResult.mjs +0 -90
  96. package/dist/watcher/writeOneSchemaFile.d.mts +0 -11
  97. 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';
5
- import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
6
- import writeOneSchemaFile from './writeOneSchemaFile.mjs';
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';
9
+ import ensureSchemaFiles, { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.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,13 @@ 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))}.`);
164
196
  // automatically watches segments and modules
165
- this.#watchConfig();
197
+ this.#watchConfig(callback);
166
198
  }
167
199
  #processControllerChange = async (filePath) => {
168
200
  const { log } = this.#projectInfo;
@@ -173,72 +205,78 @@ export class VovkCLIWatcher {
173
205
  }
174
206
  const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
175
207
  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['"]/;
208
+ const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
177
209
  if (importRegex.test(code) && namesOfClasses.length) {
178
210
  const affectedSegments = this.#segments.filter((s) => {
179
- const schema = this.#schemas[s.segmentName];
180
- if (!schema)
211
+ const segmentSchema = this.#fullSchema.segments[s.segmentName];
212
+ if (!segmentSchema)
181
213
  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]);
214
+ const controllersByOriginalName = keyBy(segmentSchema.controllers, 'originalControllerName');
215
+ return namesOfClasses.some((name) => segmentSchema.controllers[name] || controllersByOriginalName[name]);
188
216
  });
189
217
  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))}`);
218
+ 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
219
  for (const segment of affectedSegments) {
192
220
  await this.#requestSchema(segment.segmentName);
193
221
  }
194
222
  }
223
+ else {
224
+ log.debug(`The class ${namesOfClasses.join(', ')} does not belong to any segment`);
225
+ }
195
226
  }
196
227
  else {
197
- log.debug(`The file does not contain any controller or worker`);
228
+ log.debug(`The file ${filePath} does not contain any controller`);
198
229
  }
199
230
  };
200
231
  #requestSchema = debounceWithArgs(async (segmentName) => {
201
- const { apiEntryPoint, log, port, config } = this.#projectInfo;
232
+ const { apiRoot, log, port, config } = this.#projectInfo;
202
233
  const { devHttps } = config;
203
- const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
234
+ const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
204
235
  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
236
  try {
216
- ({ schema } = (await resp.json()));
237
+ const resp = await fetch(endpoint);
238
+ if (resp.status !== 200) {
239
+ const probableCause = {
240
+ 404: 'The segment did not compile or config.origin is wrong.',
241
+ }[resp.status];
242
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
243
+ return { isError: true };
244
+ }
245
+ let segmentSchema = null;
246
+ try {
247
+ ({ schema: segmentSchema } = (await resp.json()));
248
+ }
249
+ catch (error) {
250
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
251
+ }
252
+ await this.#handleSegmentSchema(segmentName, segmentSchema);
217
253
  }
218
254
  catch (error) {
219
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
255
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
256
+ return { isError: true };
220
257
  }
221
- await this.#handleSchema(schema);
258
+ return { isError: false };
222
259
  }, 500);
223
- async #handleSchema(schema) {
260
+ #generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, fullSchema: this.#fullSchema }).then(this.#onFirstTimeGenerate), 1000);
261
+ async #handleSegmentSchema(segmentName, segmentSchema) {
224
262
  const { log, config, cwd } = this.#projectInfo;
225
- if (!schema) {
226
- log.warn('Segment schema is null');
263
+ if (!segmentSchema) {
264
+ log.warn(`${formatLoggedSegmentName(segmentName)} schema is null`);
227
265
  return;
228
266
  }
229
- log.debug(`Handling received schema from ${formatLoggedSegmentName(schema.segmentName)}`);
267
+ log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
230
268
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
231
- const segment = this.#segments.find((s) => s.segmentName === schema.segmentName);
269
+ const segment = this.#segments.find((s) => s.segmentName === segmentName);
232
270
  if (!segment) {
233
- log.warn(`Segment "${schema.segmentName}" not found`);
271
+ log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
234
272
  return;
235
273
  }
236
- this.#schemas[schema.segmentName] = schema;
237
- if (schema.emitSchema) {
274
+ this.#fullSchema.segments[segmentName] = segmentSchema;
275
+ if (segmentSchema.emitSchema) {
238
276
  const now = Date.now();
239
- const { diffResult } = await writeOneSchemaFile({
277
+ const { diffResult } = await writeOneSegmentSchemaFile({
240
278
  schemaOutAbsolutePath,
241
- schema,
279
+ segmentSchema,
242
280
  skipIfExists: false,
243
281
  });
244
282
  const timeTook = Date.now() - now;
@@ -247,17 +285,24 @@ export class VovkCLIWatcher {
247
285
  log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
248
286
  }
249
287
  }
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`);
288
+ else if (segmentSchema && !isSegmentSchemaEmpty(segmentSchema)) {
289
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
252
290
  }
253
- if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
291
+ if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
254
292
  log.debug(`All segments with "emitSchema" have schema.`);
255
- await generateClient(this.#projectInfo, this.#segments, this.#schemas);
293
+ this.#generate();
256
294
  }
257
295
  }
258
- async start({ clientOutDir } = {}) {
259
- this.#projectInfo = await getProjectInfo({ clientOutDir });
296
+ async start({ exit }) {
297
+ const now = Date.now();
298
+ this.#projectInfo = await getProjectInfo();
260
299
  const { log, config, cwd, apiDir } = this.#projectInfo;
300
+ log.info('Starting...');
301
+ if (exit) {
302
+ this.#onFirstTimeGenerate = once(() => {
303
+ log.info('The schemas and the RPC client have been generated. Exiting...');
304
+ });
305
+ }
261
306
  if (config.devHttps) {
262
307
  const agent = new Agent({
263
308
  connect: {
@@ -274,18 +319,46 @@ export class VovkCLIWatcher {
274
319
  });
275
320
  const apiDirAbsolutePath = path.join(cwd, apiDir);
276
321
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
277
- this.#segments = await locateSegments(apiDirAbsolutePath);
278
- 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
322
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
323
+ await ensureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
324
+ await ensureClient(this.#projectInfo);
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,41 @@
1
+ import fs from 'node:fs/promises';
2
+ import getClientTemplates, { BuiltInTemplateName } 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 } = await getClientTemplates({
9
+ config,
10
+ cwd,
11
+ generateFrom: config.generateFrom,
12
+ });
13
+ let usedTemplateNames = [];
14
+ const defaultText = `// 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
+ let text = defaultText;
21
+ if (!existing) {
22
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
23
+ // a workaround that prevents compilation error when client is not yet generated but back-end imports fullSchema
24
+ if (Object.keys(BuiltInTemplateName).includes(templateName)) {
25
+ if (outPath.endsWith('.cjs')) {
26
+ text += '\nmodule.exports.fullSchema = {};';
27
+ }
28
+ else {
29
+ text += '\nexport const fullSchema = {};';
30
+ }
31
+ }
32
+ await fs.writeFile(outPath, outPath.endsWith('.py') ? text.replace(/\/\//g, '#') : text);
33
+ usedTemplateNames.push(templateName);
34
+ }
35
+ }
36
+ usedTemplateNames = uniq(usedTemplateNames);
37
+ if (usedTemplateNames.length) {
38
+ 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`);
39
+ }
40
+ return { written: !!usedTemplateNames.length };
41
+ }
@@ -0,0 +1,24 @@
1
+ import type { VovkStrictConfig } from 'vovk';
2
+ export declare const DEFAULT_FULL_SCHEMA_FILE_NAME = "full-schema.json";
3
+ interface ClientTemplate {
4
+ templateName: string;
5
+ templatePath: string;
6
+ outPath: string;
7
+ fullSchemaOutAbsolutePath: string | null;
8
+ origin?: string | null;
9
+ }
10
+ export declare enum BuiltInTemplateName {
11
+ ts = "ts",
12
+ main = "main",
13
+ module = "module",
14
+ fullSchema = "fullSchema"
15
+ }
16
+ export default function getClientTemplates({ config, cwd, generateFrom, }: {
17
+ config: VovkStrictConfig;
18
+ cwd: string;
19
+ generateFrom?: VovkStrictConfig['generateFrom'];
20
+ }): Promise<{
21
+ clientOutDirAbsolutePath: string;
22
+ templateFiles: ClientTemplate[];
23
+ }>;
24
+ export {};