vovk-cli 0.0.1-draft.7 → 0.0.1-draft.70

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 (81) hide show
  1. package/README.md +29 -1
  2. package/client-templates/main/main.cjs.ejs +15 -0
  3. package/client-templates/main/main.d.cts.ejs +14 -0
  4. package/client-templates/module/module.d.mts.ejs +14 -0
  5. package/client-templates/module/module.mjs.ejs +24 -0
  6. package/client-templates/python/__init__.py +276 -0
  7. package/client-templates/ts/index.ts.ejs +25 -0
  8. package/dist/dev/diffSchema.d.mts +36 -0
  9. package/dist/{watcher → dev}/diffSchema.mjs +3 -11
  10. package/dist/{watcher → dev}/ensureSchemaFiles.d.mts +3 -0
  11. package/dist/{watcher → dev}/ensureSchemaFiles.mjs +26 -15
  12. package/dist/dev/index.d.mts +6 -0
  13. package/dist/{watcher → dev}/index.mjs +129 -62
  14. package/dist/{watcher → dev}/isMetadataEmpty.mjs +1 -1
  15. package/dist/{watcher → dev}/logDiffResult.d.mts +2 -2
  16. package/dist/dev/logDiffResult.mjs +57 -0
  17. package/dist/{watcher → dev}/writeOneSchemaFile.d.mts +1 -1
  18. package/dist/{watcher → dev}/writeOneSchemaFile.mjs +2 -2
  19. package/dist/generate/ensureClient.d.mts +5 -0
  20. package/dist/generate/ensureClient.mjs +27 -0
  21. package/dist/generate/getClientTemplates.d.mts +14 -0
  22. package/dist/generate/getClientTemplates.mjs +28 -0
  23. package/dist/generate/index.d.mts +13 -0
  24. package/dist/generate/index.mjs +80 -0
  25. package/dist/getProjectInfo/getConfig.d.mts +3 -3
  26. package/dist/getProjectInfo/getConfig.mjs +8 -5
  27. package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +2 -2
  28. package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
  29. package/dist/getProjectInfo/getUserConfig.mjs +3 -1
  30. package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
  31. package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
  32. package/dist/getProjectInfo/index.d.mts +14 -5
  33. package/dist/getProjectInfo/index.mjs +21 -13
  34. package/dist/index.d.mts +1 -27
  35. package/dist/index.mjs +59 -65
  36. package/dist/init/checkTSConfigForExperimentalDecorators.mjs +2 -2
  37. package/dist/init/createConfig.d.mts +3 -4
  38. package/dist/init/createConfig.mjs +9 -8
  39. package/dist/init/getTemplateFilesFromPackage.d.mts +2 -1
  40. package/dist/init/getTemplateFilesFromPackage.mjs +4 -5
  41. package/dist/init/index.d.mts +2 -3
  42. package/dist/init/index.mjs +61 -97
  43. package/dist/init/installDependencies.d.mts +4 -1
  44. package/dist/init/installDependencies.mjs +2 -2
  45. package/dist/init/logUpdateDependenciesError.d.mts +11 -0
  46. package/dist/init/logUpdateDependenciesError.mjs +45 -0
  47. package/dist/init/updateDependenciesWithoutInstalling.d.mts +3 -2
  48. package/dist/init/updateDependenciesWithoutInstalling.mjs +13 -8
  49. package/dist/init/updateNPMScripts.d.mts +3 -1
  50. package/dist/init/updateNPMScripts.mjs +10 -6
  51. package/dist/init/updateTypeScriptConfig.mjs +2 -2
  52. package/dist/initProgram.d.mts +2 -0
  53. package/dist/initProgram.mjs +22 -0
  54. package/dist/locateSegments.d.mts +7 -1
  55. package/dist/locateSegments.mjs +9 -6
  56. package/dist/new/addClassToSegmentCode.d.mts +1 -2
  57. package/dist/new/addClassToSegmentCode.mjs +9 -5
  58. package/dist/new/addCommonTerms.mjs +1 -0
  59. package/dist/new/index.d.mts +2 -2
  60. package/dist/new/index.mjs +3 -3
  61. package/dist/new/newModule.d.mts +3 -3
  62. package/dist/new/newModule.mjs +38 -27
  63. package/dist/new/newSegment.mjs +8 -6
  64. package/dist/new/render.mjs +2 -5
  65. package/dist/postinstall.mjs +16 -19
  66. package/dist/types.d.mts +48 -9
  67. package/dist/utils/debounceWithArgs.d.mts +1 -1
  68. package/dist/utils/debounceWithArgs.mjs +24 -9
  69. package/dist/utils/formatLoggedSegmentName.mjs +1 -1
  70. package/dist/utils/getAvailablePort.mjs +3 -2
  71. package/dist/utils/getFileSystemEntryType.mjs +1 -1
  72. package/package.json +21 -18
  73. package/templates/controller.ejs +12 -11
  74. package/templates/service.ejs +6 -6
  75. package/dist/generateClient.d.mts +0 -7
  76. package/dist/generateClient.mjs +0 -97
  77. package/dist/watcher/diffSchema.d.mts +0 -43
  78. package/dist/watcher/index.d.mts +0 -6
  79. package/dist/watcher/logDiffResult.mjs +0 -90
  80. package/templates/worker.ejs +0 -1
  81. /package/dist/{watcher → dev}/isMetadataEmpty.d.mts +0 -0
@@ -1,33 +1,36 @@
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 isEmpty from 'lodash/isEmpty.js';
9
+ import once from 'lodash/once.js';
5
10
  import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
6
11
  import writeOneSchemaFile from './writeOneSchemaFile.mjs';
7
12
  import logDiffResult from './logDiffResult.mjs';
8
- import generateClient from '../generateClient.mjs';
13
+ import ensureClient from '../generate/ensureClient.mjs';
14
+ import getProjectInfo from '../getProjectInfo/index.mjs';
15
+ import generate from '../generate/index.mjs';
9
16
  import locateSegments from '../locateSegments.mjs';
10
17
  import debounceWithArgs from '../utils/debounceWithArgs.mjs';
11
- import debounce from 'lodash/debounce.js';
12
- import isEmpty from 'lodash/isEmpty.js';
13
18
  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 {
19
+ export class VovkDev {
18
20
  #projectInfo;
19
21
  #segments = [];
20
22
  #schemas = {};
21
23
  #isWatching = false;
22
24
  #modulesWatcher = null;
23
25
  #segmentWatcher = null;
24
- #watchSegments = () => {
26
+ #onFirstTimeGenerate = null;
27
+ #watchSegments = (callback) => {
25
28
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
26
29
  const { cwd, log, config, apiDir } = this.#projectInfo;
27
30
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
28
31
  const apiDirAbsolutePath = path.join(cwd, apiDir);
29
32
  const getSegmentName = (filePath) => path.relative(apiDirAbsolutePath, filePath).replace(segmentReg, '');
30
- log.debug(`Watching segments in ${apiDirAbsolutePath}`);
33
+ log.debug(`Watching segments at ${apiDirAbsolutePath}`);
31
34
  this.#segmentWatcher = chokidar
32
35
  .watch(apiDirAbsolutePath, {
33
36
  persistent: true,
@@ -39,7 +42,14 @@ export class VovkCLIWatcher {
39
42
  const segmentName = getSegmentName(filePath);
40
43
  this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
41
44
  ? this.#segments
42
- : [...this.#segments, { routeFilePath: filePath, segmentName }];
45
+ : [
46
+ ...this.#segments,
47
+ {
48
+ routeFilePath: filePath,
49
+ segmentName,
50
+ segmentImportPath: path.relative(config.clientOutDir, filePath), // TODO DRY locateSegments
51
+ },
52
+ ];
43
53
  log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
44
54
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
45
55
  void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
@@ -53,14 +63,14 @@ export class VovkCLIWatcher {
53
63
  })
54
64
  .on('addDir', async (dirPath) => {
55
65
  log.debug(`Directory ${dirPath} has been added to segments folder`);
56
- this.#segments = await locateSegments(apiDirAbsolutePath);
66
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
57
67
  for (const { segmentName } of this.#segments) {
58
68
  void this.#requestSchema(segmentName);
59
69
  }
60
70
  })
61
71
  .on('unlinkDir', async (dirPath) => {
62
72
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
63
- this.#segments = await locateSegments(apiDirAbsolutePath);
73
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
64
74
  for (const { segmentName } of this.#segments) {
65
75
  void this.#requestSchema(segmentName);
66
76
  }
@@ -76,16 +86,17 @@ export class VovkCLIWatcher {
76
86
  }
77
87
  })
78
88
  .on('ready', () => {
89
+ callback();
79
90
  log.debug('Segments watcher is ready');
80
91
  })
81
92
  .on('error', (error) => {
82
93
  log.error(`Error watching segments folder: ${error?.message ?? 'Unknown error'}`);
83
94
  });
84
95
  };
85
- #watchModules = () => {
96
+ #watchModules = (callback) => {
86
97
  const { config, cwd, log } = this.#projectInfo;
87
98
  const modulesDirAbsolutePath = path.join(cwd, config.modulesDir);
88
- log.debug(`Watching modules in ${modulesDirAbsolutePath}`);
99
+ log.debug(`Watching modules at ${modulesDirAbsolutePath}`);
89
100
  const processControllerChange = debounceWithArgs(this.#processControllerChange, 500);
90
101
  this.#modulesWatcher = chokidar
91
102
  .watch(modulesDirAbsolutePath, {
@@ -114,30 +125,45 @@ export class VovkCLIWatcher {
114
125
  }
115
126
  })
116
127
  .on('ready', () => {
128
+ callback();
117
129
  log.debug('Modules watcher is ready');
118
130
  })
119
131
  .on('error', (error) => {
120
132
  log.error(`Error watching modules folder: ${error?.message ?? 'Unknown error'}`);
121
133
  });
122
134
  };
123
- #watchConfig = () => {
135
+ #watchConfig = (callback) => {
124
136
  const { log, cwd } = this.#projectInfo;
125
137
  log.debug(`Watching config files`);
126
138
  let isInitial = true;
127
139
  let isReady = false;
128
140
  const handle = debounce(async () => {
129
141
  this.#projectInfo = await getProjectInfo();
130
- if (!isInitial) {
131
- log.info('Config file has been updated');
132
- isInitial = false;
133
- }
134
142
  await this.#modulesWatcher?.close();
135
143
  await this.#segmentWatcher?.close();
136
- this.#watchModules();
137
- this.#watchSegments();
144
+ await Promise.all([
145
+ new Promise((resolve) => this.#watchModules(() => resolve(0))),
146
+ new Promise((resolve) => this.#watchSegments(() => resolve(0))),
147
+ ]);
148
+ if (isInitial) {
149
+ callback();
150
+ }
151
+ else {
152
+ log.info('Config file has been updated');
153
+ this.#generate();
154
+ }
155
+ isInitial = false;
138
156
  }, 1000);
139
157
  chokidar
140
- .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
158
+ // .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
159
+ .watch([
160
+ 'vovk.config.js',
161
+ 'vovk.config.mjs',
162
+ 'vovk.config.cjs',
163
+ '.config/vovk.config.js',
164
+ '.config/vovk.config.mjs',
165
+ '.config/vovk.config.cjs',
166
+ ], {
141
167
  persistent: true,
142
168
  cwd,
143
169
  ignoreInitial: false,
@@ -156,13 +182,14 @@ export class VovkCLIWatcher {
156
182
  .on('error', (error) => log.error(`Error watching config files: ${error?.message ?? 'Unknown error'}`));
157
183
  void handle();
158
184
  };
159
- #watch() {
185
+ async #watch(callback) {
160
186
  if (this.#isWatching)
161
187
  throw new Error('Already watching');
162
188
  const { log } = this.#projectInfo;
163
189
  log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
190
+ await ensureClient(this.#projectInfo);
164
191
  // automatically watches segments and modules
165
- this.#watchConfig();
192
+ this.#watchConfig(callback);
166
193
  }
167
194
  #processControllerChange = async (filePath) => {
168
195
  const { log } = this.#projectInfo;
@@ -173,53 +200,59 @@ export class VovkCLIWatcher {
173
200
  }
174
201
  const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
175
202
  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['"]/;
203
+ const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
177
204
  if (importRegex.test(code) && namesOfClasses.length) {
178
205
  const affectedSegments = this.#segments.filter((s) => {
179
206
  const schema = this.#schemas[s.segmentName];
180
207
  if (!schema)
181
208
  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]);
209
+ const controllersByOriginalName = keyBy(schema.controllers, 'originalControllerName');
210
+ return namesOfClasses.some((name) => schema.controllers[name] || controllersByOriginalName[name]);
188
211
  });
189
212
  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))}`);
213
+ 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
214
  for (const segment of affectedSegments) {
192
215
  await this.#requestSchema(segment.segmentName);
193
216
  }
194
217
  }
218
+ else {
219
+ log.debug(`The class ${namesOfClasses.join(', ')} does not belong to any segment`);
220
+ }
195
221
  }
196
222
  else {
197
- log.debug(`The file does not contain any controller or worker`);
223
+ log.debug(`The file does not contain any controller`);
198
224
  }
199
225
  };
200
226
  #requestSchema = debounceWithArgs(async (segmentName) => {
201
- const { apiEntryPoint, log, port, config } = this.#projectInfo;
227
+ const { apiRoot, log, port, config } = this.#projectInfo;
202
228
  const { devHttps } = config;
203
- const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
229
+ const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
204
230
  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
231
  try {
216
- ({ schema } = (await resp.json()));
232
+ const resp = await fetch(endpoint);
233
+ if (resp.status !== 200) {
234
+ const probableCause = {
235
+ 404: 'The segment did not compile or config.origin is wrong.',
236
+ }[resp.status];
237
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
238
+ return { isError: true };
239
+ }
240
+ let schema = null;
241
+ try {
242
+ ({ schema } = (await resp.json()));
243
+ }
244
+ catch (error) {
245
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
246
+ }
247
+ await this.#handleSchema(schema);
217
248
  }
218
249
  catch (error) {
219
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
250
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
251
+ return { isError: true };
220
252
  }
221
- await this.#handleSchema(schema);
253
+ return { isError: false };
222
254
  }, 500);
255
+ #generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, segmentsSchema: this.#schemas }).then(this.#onFirstTimeGenerate), 1000);
223
256
  async #handleSchema(schema) {
224
257
  const { log, config, cwd } = this.#projectInfo;
225
258
  if (!schema) {
@@ -247,17 +280,24 @@ export class VovkCLIWatcher {
247
280
  log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
248
281
  }
249
282
  }
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`);
283
+ else if (schema && !isEmpty(schema.controllers)) {
284
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
252
285
  }
253
286
  if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
254
287
  log.debug(`All segments with "emitSchema" have schema.`);
255
- await generateClient(this.#projectInfo, this.#segments, this.#schemas);
288
+ this.#generate();
256
289
  }
257
290
  }
258
- async start({ clientOutDir } = {}) {
259
- this.#projectInfo = await getProjectInfo({ clientOutDir });
291
+ async start({ exit }) {
292
+ const now = Date.now();
293
+ this.#projectInfo = await getProjectInfo();
260
294
  const { log, config, cwd, apiDir } = this.#projectInfo;
295
+ log.info('Starting...');
296
+ if (exit) {
297
+ this.#onFirstTimeGenerate = once(() => {
298
+ log.info('The schemas and the RPC client have been generated. Exiting...');
299
+ });
300
+ }
261
301
  if (config.devHttps) {
262
302
  const agent = new Agent({
263
303
  connect: {
@@ -274,18 +314,45 @@ export class VovkCLIWatcher {
274
314
  });
275
315
  const apiDirAbsolutePath = path.join(cwd, apiDir);
276
316
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
277
- this.#segments = await locateSegments(apiDirAbsolutePath);
317
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
278
318
  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
319
+ const MAX_ATTEMPTS = 5;
320
+ const DELAY = 5000;
321
+ // Request schema every segment in 5 seconds in order to update schema on start
280
322
  setTimeout(() => {
281
323
  for (const { segmentName } of this.#segments) {
282
- void this.#requestSchema(segmentName);
324
+ let attempts = 0;
325
+ void this.#requestSchema(segmentName).then(({ isError }) => {
326
+ if (isError) {
327
+ const interval = setInterval(() => {
328
+ attempts++;
329
+ if (attempts >= MAX_ATTEMPTS) {
330
+ clearInterval(interval);
331
+ log.error(`Failed to request schema for ${formatLoggedSegmentName(segmentName)} after ${MAX_ATTEMPTS} attempts`);
332
+ return;
333
+ }
334
+ void this.#requestSchema(segmentName).then(({ isError: isError2 }) => {
335
+ if (!isError2) {
336
+ clearInterval(interval);
337
+ log.info(`Requested schema for ${formatLoggedSegmentName(segmentName)} after ${attempts} attempts`);
338
+ }
339
+ });
340
+ }, DELAY);
341
+ }
342
+ });
283
343
  }
284
- this.#watch();
285
- }, 5000);
344
+ }, DELAY);
345
+ if (!exit) {
346
+ this.#watch(() => {
347
+ log.info(`Ready in ${Date.now() - now}ms. Making initial requests for schemas in a moment...`);
348
+ });
349
+ }
350
+ else {
351
+ log.info(`Ready in ${Date.now() - now}ms. Making requests for schemas in a moment...`);
352
+ }
286
353
  }
287
354
  }
288
355
  const env = process.env;
289
356
  if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
290
- void new VovkCLIWatcher().start();
357
+ void new VovkDev().start({ exit: env.__VOVK_EXIT__ === 'true' });
291
358
  }
@@ -1,4 +1,4 @@
1
1
  import isEmpty from 'lodash/isEmpty.js';
2
2
  export default function isSchemaEmpty(schema) {
3
- return isEmpty(schema.controllers) && isEmpty(schema.workers);
3
+ return isEmpty(schema.controllers);
4
4
  }
@@ -1,3 +1,3 @@
1
- import { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- import { DiffResult } from './diffSchema.mjs';
1
+ import type { DiffResult } from './diffSchema.mjs';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
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 < 12 ? diffNormalized.length : 10;
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
+ }
@@ -1,5 +1,5 @@
1
1
  import type { VovkSchema } from 'vovk';
2
- import { DiffResult } from './diffSchema.mjs';
2
+ import { type DiffResult } from './diffSchema.mjs';
3
3
  export declare const ROOT_SEGMENT_SCHEMA_NAME = "_root";
4
4
  export default function writeOneSchemaFile({ schemaOutAbsolutePath, schema, skipIfExists, }: {
5
5
  schemaOutAbsolutePath: string;
@@ -1,5 +1,5 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
3
  import diffSchema from './diffSchema.mjs';
4
4
  import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
5
5
  export const ROOT_SEGMENT_SCHEMA_NAME = '_root';
@@ -0,0 +1,5 @@
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function ensureClient({ config, cwd, log }: ProjectInfo): Promise<{
3
+ written: boolean;
4
+ path: string;
5
+ }>;
@@ -0,0 +1,27 @@
1
+ import fs from 'node:fs/promises';
2
+ import getClientTemplates from './getClientTemplates.mjs';
3
+ export default async function ensureClient({ config, cwd, log }) {
4
+ const now = Date.now();
5
+ const { clientOutDirAbsolutePath, templateFiles } = getClientTemplates({
6
+ config,
7
+ cwd,
8
+ templateNames: config.experimental_clientGenerateTemplateNames,
9
+ });
10
+ const text = `// auto-generated ${new Date().toISOString()}
11
+ // This is a temporary placeholder to avoid errors if client is imported before it's generated.
12
+ // If you still see this text, the client is not generated yet because of an unknown problem.
13
+ // Feel free to report an issue at https://github.com/finom/vovk/issues`;
14
+ let written = false;
15
+ for (const { outPath } of templateFiles) {
16
+ const existing = await fs.readFile(outPath, 'utf-8').catch(() => null);
17
+ if (!existing) {
18
+ await fs.mkdir(clientOutDirAbsolutePath, { recursive: true });
19
+ await fs.writeFile(outPath, text);
20
+ written = true;
21
+ }
22
+ }
23
+ if (written) {
24
+ log.info(`Empty client files are generated in ${Date.now() - now}ms`);
25
+ }
26
+ return { written, path: clientOutDirAbsolutePath };
27
+ }
@@ -0,0 +1,14 @@
1
+ import { VovkConfig } from '../types.mjs';
2
+ interface ClientTemplate {
3
+ templatePath: string;
4
+ outPath: string;
5
+ }
6
+ export default function getClientTemplates({ config, cwd, templateNames, }: {
7
+ config: Required<VovkConfig>;
8
+ cwd: string;
9
+ templateNames?: string[];
10
+ }): {
11
+ clientOutDirAbsolutePath: string;
12
+ templateFiles: ClientTemplate[];
13
+ };
14
+ export {};
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ export default function getClientTemplates({ config, cwd, templateNames = [], }) {
3
+ const templatesDir = path.join(import.meta.dirname, '../..', 'client-templates');
4
+ const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
5
+ const mapper = (dir) => (name) => ({
6
+ templatePath: path.resolve(templatesDir, dir, name),
7
+ outPath: path.join(clientOutDirAbsolutePath, name.replace('.ejs', '')),
8
+ });
9
+ const builtInTemplatesMap = {
10
+ ts: ['index.ts.ejs'].map(mapper('ts')),
11
+ main: ['main.cjs.ejs', 'main.d.cts.ejs'].map(mapper('main')),
12
+ module: ['module.mjs.ejs', 'module.d.mts.ejs'].map(mapper('module')),
13
+ python: ['__init__.py'].map(mapper('python')),
14
+ };
15
+ const templateFiles = (templateNames ?? config.experimental_clientGenerateTemplateNames).reduce((acc, template) => {
16
+ if (template in builtInTemplatesMap) {
17
+ return [...acc, ...builtInTemplatesMap[template]];
18
+ }
19
+ return [
20
+ ...acc,
21
+ {
22
+ templatePath: path.resolve(cwd, template),
23
+ outPath: path.join(clientOutDirAbsolutePath, path.basename(template).replace('.ejs', '')),
24
+ },
25
+ ];
26
+ }, []);
27
+ return { clientOutDirAbsolutePath, templateFiles };
28
+ }
@@ -0,0 +1,13 @@
1
+ import type { VovkSchema } from 'vovk';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ import type { Segment } from '../locateSegments.mjs';
4
+ import { GenerateOptions } from '../types.mjs';
5
+ export default function generate({ projectInfo, segments, segmentsSchema, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, }: {
6
+ projectInfo: ProjectInfo;
7
+ segments: Segment[];
8
+ segmentsSchema: Record<string, VovkSchema>;
9
+ forceNothingWrittenLog?: boolean;
10
+ } & Pick<GenerateOptions, 'templates' | 'prettify' | 'fullSchema'>): Promise<{
11
+ written: boolean;
12
+ path: string;
13
+ }>;
@@ -0,0 +1,80 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import ejs from 'ejs';
4
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
5
+ import prettify from '../utils/prettify.mjs';
6
+ import getClientTemplates from './getClientTemplates.mjs';
7
+ export default async function generate({ projectInfo, segments, segmentsSchema, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, }) {
8
+ templates = templates ?? projectInfo.config.experimental_clientGenerateTemplateNames;
9
+ const noClient = templates?.[0] === 'none';
10
+ const { config, cwd, log, clientImports, apiRoot } = projectInfo;
11
+ const { clientOutDirAbsolutePath, templateFiles } = getClientTemplates({ config, cwd, templateNames: templates });
12
+ // Ensure that each segment has a matching schema if it needs to be emitted:
13
+ for (let i = 0; i < segments.length; i++) {
14
+ const { segmentName } = segments[i];
15
+ const schema = segmentsSchema[segmentName];
16
+ if (!schema) {
17
+ throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
18
+ }
19
+ if (!schema.emitSchema)
20
+ continue;
21
+ }
22
+ const now = Date.now();
23
+ // Data for the EJS templates:
24
+ const template = {
25
+ apiRoot,
26
+ imports: clientImports,
27
+ segments,
28
+ fullSchema: segmentsSchema,
29
+ };
30
+ // Process each template in parallel
31
+ const processedTemplates = noClient
32
+ ? []
33
+ : await Promise.all(templateFiles.map(async ({ templatePath, outPath }) => {
34
+ // Read the EJS template
35
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
36
+ // Render the template
37
+ let rendered = templatePath.endsWith('.ejs')
38
+ ? ejs.render(templateContent, { template }, {
39
+ filename: templatePath,
40
+ })
41
+ : templateContent;
42
+ // Optionally prettify
43
+ if (prettifyClient || config.prettifyClient) {
44
+ rendered = await prettify(rendered, outPath);
45
+ }
46
+ // Read existing file content to compare
47
+ const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
48
+ // Determine if we need to rewrite the file
49
+ const needsWriting = existingContent !== rendered;
50
+ return {
51
+ outPath,
52
+ rendered,
53
+ needsWriting,
54
+ };
55
+ }));
56
+ const anyNeedsWriting = processedTemplates.some(({ needsWriting }) => needsWriting);
57
+ if (fullSchema || anyNeedsWriting) {
58
+ // Make sure the output directory exists
59
+ await fs.mkdir(clientOutDirAbsolutePath, { recursive: true });
60
+ }
61
+ if (fullSchema) {
62
+ const fullSchemaOutAbsolutePath = path.resolve(clientOutDirAbsolutePath, typeof fullSchema === 'string' ? fullSchema : 'full-schema.json');
63
+ await fs.writeFile(fullSchemaOutAbsolutePath, JSON.stringify(segmentsSchema, null, 2));
64
+ log.info(`Full schema has ben written to ${fullSchemaOutAbsolutePath}`);
65
+ }
66
+ if (!anyNeedsWriting) {
67
+ const logOrDebug = forceNothingWrittenLog ? log.info : log.debug;
68
+ logOrDebug(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
69
+ return { written: false, path: clientOutDirAbsolutePath };
70
+ }
71
+ // Write updated files where needed
72
+ await Promise.all(processedTemplates.map(({ outPath, rendered, needsWriting }) => {
73
+ if (needsWriting) {
74
+ return fs.writeFile(outPath, rendered);
75
+ }
76
+ return null;
77
+ }));
78
+ log.info(`Client generated in ${Date.now() - now}ms`);
79
+ return { written: true, path: clientOutDirAbsolutePath };
80
+ }
@@ -1,11 +1,11 @@
1
- import type { VovkConfig } from '../types.mjs';
1
+ import type { VovkStrictConfig } from '../types.mjs';
2
2
  export default function getConfig({ clientOutDir, cwd }: {
3
3
  clientOutDir?: string;
4
4
  cwd: string;
5
5
  }): Promise<{
6
- config: Required<VovkConfig>;
6
+ config: VovkStrictConfig;
7
7
  srcRoot: string;
8
8
  configAbsolutePaths: string[];
9
- userConfig: VovkConfig | null;
9
+ userConfig: import("../types.mjs").VovkConfig | null;
10
10
  error: Error | undefined;
11
11
  }>;
@@ -5,23 +5,26 @@ export default async function getConfig({ clientOutDir, cwd }) {
5
5
  const { configAbsolutePaths, error, userConfig } = await getUserConfig({ cwd });
6
6
  const conf = userConfig ?? {};
7
7
  const srcRoot = await getRelativeSrcRoot({ cwd });
8
+ const validateOnClientImport = env.VOVK_VALIDATE_ON_CLIENT_PATH ?? conf.validateOnClientImport ?? null;
9
+ const fetcherImport = env.VOVK_FETCHER_PATH ?? conf.fetcherImport ?? 'vovk/dist/client/defaultFetcher.js';
10
+ const createRPCImport = env.VOVK_CREATE_RPC_PATH ?? conf.createRPCImport ?? 'vovk/dist/client/createRPC.js';
8
11
  const config = {
9
12
  modulesDir: env.VOVK_MODULES_DIR ?? conf.modulesDir ?? './' + [srcRoot, 'modules'].filter(Boolean).join('/'),
10
- validateOnClient: env.VOVK_VALIDATE_ON_CLIENT ?? conf.validateOnClient ?? null,
11
- validationLibrary: env.VOVK_VALIDATION_LIBRARY ?? conf.validationLibrary ?? null,
12
- fetcher: env.VOVK_FETCHER ?? conf.fetcher ?? 'vovk/client/defaultFetcher',
13
+ validateOnClientImport: typeof validateOnClientImport === 'string' ? [validateOnClientImport] : validateOnClientImport,
14
+ fetcherImport: typeof fetcherImport === 'string' ? [fetcherImport] : fetcherImport,
15
+ createRPCImport: typeof createRPCImport === 'string' ? [createRPCImport] : createRPCImport,
13
16
  schemaOutDir: env.VOVK_SCHEMA_OUT_DIR ?? conf.schemaOutDir ?? './.vovk-schema',
14
17
  clientOutDir: clientOutDir ?? env.VOVK_CLIENT_OUT_DIR ?? conf.clientOutDir ?? './node_modules/.vovk-client',
15
18
  origin: (env.VOVK_ORIGIN ?? conf.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
16
19
  rootEntry: env.VOVK_ROOT_ENTRY ?? conf.rootEntry ?? 'api',
17
20
  rootSegmentModulesDirName: env.VOVK_ROOT_SEGMENT_MODULES_DIR_NAME ?? conf.rootSegmentModulesDirName ?? '',
18
- logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'debug', // TODO: change to 'warn' when v3 is ready
21
+ logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'info',
19
22
  prettifyClient: (env.VOVK_PRETTIFY_CLIENT ? !!env.VOVK_PRETTIFY_CLIENT : null) ?? conf.prettifyClient ?? false,
20
23
  devHttps: (env.VOVK_DEV_HTTPS ? !!env.VOVK_DEV_HTTPS : null) ?? conf.devHttps ?? false,
24
+ experimental_clientGenerateTemplateNames: conf.experimental_clientGenerateTemplateNames ?? ['ts', 'module', 'main'],
21
25
  templates: {
22
26
  service: 'vovk-cli/templates/service.ejs',
23
27
  controller: 'vovk-cli/templates/controller.ejs',
24
- worker: 'vovk-cli/templates/worker.ejs',
25
28
  ...conf.templates,
26
29
  },
27
30
  };