vovk-cli 0.0.1-draft.35 → 0.0.1-draft.351

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 (140) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -1
  3. package/client-templates/cjs/index.cjs.ejs +20 -0
  4. package/client-templates/cjs/index.d.cts.ejs +22 -0
  5. package/client-templates/mixins/mixins.d.ts.ejs +64 -0
  6. package/client-templates/mixins/mixins.json.ejs +1 -0
  7. package/client-templates/mjs/index.d.mts.ejs +22 -0
  8. package/client-templates/mjs/index.mjs.ejs +18 -0
  9. package/client-templates/openapiCjs/openapi.cjs.ejs +4 -0
  10. package/client-templates/openapiCjs/openapi.d.cts.ejs +4 -0
  11. package/client-templates/openapiJson/openapi.json.ejs +1 -0
  12. package/client-templates/openapiTs/openapi.ts.ejs +4 -0
  13. package/client-templates/packageJson/package.json.ejs +1 -0
  14. package/client-templates/readme/README.md.ejs +39 -0
  15. package/client-templates/schemaCjs/schema.cjs.ejs +23 -0
  16. package/client-templates/schemaCjs/schema.d.cts.ejs +10 -0
  17. package/client-templates/schemaJson/schema.json.ejs +1 -0
  18. package/client-templates/schemaTs/schema.ts.ejs +31 -0
  19. package/client-templates/ts/index.ts.ejs +27 -0
  20. package/dist/bundle/index.d.mts +8 -0
  21. package/dist/bundle/index.mjs +102 -0
  22. package/dist/dev/diffSegmentSchema.d.mts +36 -0
  23. package/dist/dev/{diffSchema.mjs → diffSegmentSchema.mjs} +3 -11
  24. package/dist/dev/ensureSchemaFiles.d.mts +4 -1
  25. package/dist/dev/ensureSchemaFiles.mjs +15 -31
  26. package/dist/dev/index.d.mts +5 -1
  27. package/dist/dev/index.mjs +192 -80
  28. package/dist/dev/logDiffResult.d.mts +1 -1
  29. package/dist/dev/logDiffResult.mjs +6 -43
  30. package/dist/dev/writeMetaJson.d.mts +2 -0
  31. package/dist/dev/writeMetaJson.mjs +20 -0
  32. package/dist/dev/writeOneSegmentSchemaFile.d.mts +12 -0
  33. package/dist/dev/{writeOneSchemaFile.mjs → writeOneSegmentSchemaFile.mjs} +10 -6
  34. package/dist/generate/ensureClient.d.mts +3 -0
  35. package/dist/generate/ensureClient.mjs +28 -0
  36. package/dist/generate/generate.d.mts +13 -0
  37. package/dist/generate/generate.mjs +296 -0
  38. package/dist/generate/getClientTemplateFiles.d.mts +20 -0
  39. package/dist/generate/getClientTemplateFiles.mjs +85 -0
  40. package/dist/generate/getProjectFullSchema.d.mts +8 -0
  41. package/dist/generate/getProjectFullSchema.mjs +64 -0
  42. package/dist/generate/getTemplateClientImports.d.mts +19 -0
  43. package/dist/generate/getTemplateClientImports.mjs +49 -0
  44. package/dist/generate/index.d.mts +33 -0
  45. package/dist/generate/index.mjs +190 -0
  46. package/dist/generate/writeOneClientFile.d.mts +41 -0
  47. package/dist/generate/writeOneClientFile.mjs +136 -0
  48. package/dist/getProjectInfo/getConfig/getConfigAbsolutePaths.d.mts +5 -0
  49. package/dist/getProjectInfo/{getConfigAbsolutePaths.mjs → getConfig/getConfigAbsolutePaths.mjs} +4 -1
  50. package/dist/getProjectInfo/{getRelativeSrcRoot.d.mts → getConfig/getRelativeSrcRoot.d.mts} +1 -1
  51. package/dist/getProjectInfo/{getRelativeSrcRoot.mjs → getConfig/getRelativeSrcRoot.mjs} +2 -2
  52. package/dist/getProjectInfo/getConfig/getTemplateDefs.d.mts +24 -0
  53. package/dist/getProjectInfo/getConfig/getTemplateDefs.mjs +165 -0
  54. package/dist/getProjectInfo/{getUserConfig.d.mts → getConfig/getUserConfig.d.mts} +3 -2
  55. package/dist/getProjectInfo/{getUserConfig.mjs → getConfig/getUserConfig.mjs} +3 -3
  56. package/dist/getProjectInfo/{importUncachedModule.mjs → getConfig/importUncachedModule.mjs} +1 -4
  57. package/dist/getProjectInfo/getConfig/index.d.mts +76 -0
  58. package/dist/getProjectInfo/getConfig/index.mjs +91 -0
  59. package/dist/getProjectInfo/getMetaSchema.d.mts +8 -0
  60. package/dist/getProjectInfo/getMetaSchema.mjs +14 -0
  61. package/dist/getProjectInfo/index.d.mts +14 -9
  62. package/dist/getProjectInfo/index.mjs +24 -22
  63. package/dist/index.d.mts +2 -2
  64. package/dist/index.mjs +118 -36
  65. package/dist/init/createConfig.d.mts +2 -2
  66. package/dist/init/createConfig.mjs +39 -13
  67. package/dist/init/createStandardSchemaValidatorFile.d.mts +4 -0
  68. package/dist/init/createStandardSchemaValidatorFile.mjs +39 -0
  69. package/dist/init/getTemplateFilesFromPackage.mjs +10 -5
  70. package/dist/init/index.d.mts +2 -2
  71. package/dist/init/index.mjs +114 -66
  72. package/dist/init/installDependencies.mjs +4 -2
  73. package/dist/init/logUpdateDependenciesError.d.mts +3 -1
  74. package/dist/init/logUpdateDependenciesError.mjs +7 -1
  75. package/dist/init/updateDependenciesWithoutInstalling.mjs +39 -9
  76. package/dist/init/updateNPMScripts.d.mts +3 -1
  77. package/dist/init/updateNPMScripts.mjs +10 -7
  78. package/dist/init/updateTypeScriptConfig.d.mts +4 -1
  79. package/dist/init/updateTypeScriptConfig.mjs +11 -7
  80. package/dist/initProgram.d.mts +1 -1
  81. package/dist/initProgram.mjs +17 -17
  82. package/dist/locateSegments.d.mts +8 -1
  83. package/dist/locateSegments.mjs +13 -3
  84. package/dist/new/addClassToSegmentCode.d.mts +1 -2
  85. package/dist/new/addClassToSegmentCode.mjs +3 -3
  86. package/dist/new/index.d.mts +2 -1
  87. package/dist/new/index.mjs +4 -2
  88. package/dist/new/newModule.d.mts +4 -1
  89. package/dist/new/newModule.mjs +18 -17
  90. package/dist/new/newSegment.d.mts +4 -1
  91. package/dist/new/newSegment.mjs +19 -11
  92. package/dist/new/render.d.mts +7 -3
  93. package/dist/new/render.mjs +29 -8
  94. package/dist/types.d.mts +64 -42
  95. package/dist/utils/compileJSONSchemaToTypeScriptType.d.mts +5 -0
  96. package/dist/utils/compileJSONSchemaToTypeScriptType.mjs +9 -0
  97. package/dist/utils/compileTs.d.mts +12 -0
  98. package/dist/utils/compileTs.mjs +261 -0
  99. package/dist/utils/debounceWithArgs.d.mts +2 -2
  100. package/dist/utils/debounceWithArgs.mjs +24 -6
  101. package/dist/utils/formatLoggedSegmentName.d.mts +3 -1
  102. package/dist/utils/formatLoggedSegmentName.mjs +3 -2
  103. package/dist/utils/generateFnName.d.mts +23 -0
  104. package/dist/utils/generateFnName.mjs +76 -0
  105. package/dist/utils/getPackageJson.d.mts +3 -0
  106. package/dist/utils/getPackageJson.mjs +22 -0
  107. package/dist/utils/getPublicModuleNameFromPath.d.mts +4 -0
  108. package/dist/utils/getPublicModuleNameFromPath.mjs +9 -0
  109. package/dist/utils/normalizeOpenAPIMixin.d.mts +14 -0
  110. package/dist/utils/normalizeOpenAPIMixin.mjs +114 -0
  111. package/dist/utils/pickSegmentFullSchema.d.mts +3 -0
  112. package/dist/utils/pickSegmentFullSchema.mjs +15 -0
  113. package/dist/utils/removeUnlistedDirectories.d.mts +10 -0
  114. package/dist/utils/removeUnlistedDirectories.mjs +61 -0
  115. package/dist/utils/resolveAbsoluteModulePath.d.mts +2 -0
  116. package/dist/utils/resolveAbsoluteModulePath.mjs +32 -0
  117. package/module-templates/arktype/controller.ts.ejs +68 -0
  118. package/module-templates/type/controller.ts.ejs +56 -0
  119. package/module-templates/type/service.ts.ejs +28 -0
  120. package/module-templates/valibot/controller.ts.ejs +68 -0
  121. package/package.json +35 -22
  122. package/dist/dev/diffSchema.d.mts +0 -43
  123. package/dist/dev/ensureClient.d.mts +0 -5
  124. package/dist/dev/ensureClient.mjs +0 -31
  125. package/dist/dev/isMetadataEmpty.d.mts +0 -2
  126. package/dist/dev/isMetadataEmpty.mjs +0 -4
  127. package/dist/dev/writeOneSchemaFile.d.mts +0 -11
  128. package/dist/generateClient.d.mts +0 -7
  129. package/dist/generateClient.mjs +0 -97
  130. package/dist/getProjectInfo/getConfig.d.mts +0 -11
  131. package/dist/getProjectInfo/getConfig.mjs +0 -29
  132. package/dist/getProjectInfo/getConfigAbsolutePaths.d.mts +0 -4
  133. package/dist/postinstall.d.mts +0 -1
  134. package/dist/postinstall.mjs +0 -24
  135. package/templates/controller.ejs +0 -52
  136. package/templates/service.ejs +0 -27
  137. package/templates/worker.ejs +0 -24
  138. /package/dist/getProjectInfo/{importUncachedModule.d.mts → getConfig/importUncachedModule.d.mts} +0 -0
  139. /package/dist/getProjectInfo/{importUncachedModuleWorker.d.mts → getConfig/importUncachedModuleWorker.d.mts} +0 -0
  140. /package/dist/getProjectInfo/{importUncachedModuleWorker.mjs → getConfig/importUncachedModuleWorker.mjs} +0 -0
@@ -1,4 +1,8 @@
1
+ import type { DevOptions } from '../types.mjs';
1
2
  export declare class VovkDev {
2
3
  #private;
3
- start(): Promise<void>;
4
+ constructor({ schemaOut, devHttps, logLevel }: Pick<DevOptions, 'schemaOut' | 'devHttps' | 'logLevel'>);
5
+ start({ exit }: {
6
+ exit: boolean;
7
+ }): Promise<void>;
4
8
  }
@@ -1,32 +1,48 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { VovkSchemaIdEnum } from 'vovk';
3
4
  import * as chokidar from 'chokidar';
4
5
  import { Agent, setGlobalDispatcher } from 'undici';
5
6
  import keyBy from 'lodash/keyBy.js';
6
7
  import capitalize from 'lodash/capitalize.js';
7
8
  import debounce from 'lodash/debounce.js';
9
+ import once from 'lodash/once.js';
8
10
  import isEmpty from 'lodash/isEmpty.js';
9
- import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
10
- import writeOneSchemaFile from './writeOneSchemaFile.mjs';
11
+ import ensureSchemaFiles, { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
12
+ import writeOneSegmentSchemaFile from './writeOneSegmentSchemaFile.mjs';
11
13
  import logDiffResult from './logDiffResult.mjs';
12
- import ensureClient from './ensureClient.mjs';
14
+ import ensureClient from '../generate/ensureClient.mjs';
13
15
  import getProjectInfo from '../getProjectInfo/index.mjs';
14
- import generateClient from '../generateClient.mjs';
15
- import locateSegments from '../locateSegments.mjs';
16
+ import { generate } from '../generate/generate.mjs';
17
+ import { locateSegments } from '../locateSegments.mjs';
16
18
  import debounceWithArgs from '../utils/debounceWithArgs.mjs';
17
19
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
20
+ import writeMetaJson from './writeMetaJson.mjs';
21
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
22
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
18
23
  export class VovkDev {
19
24
  #projectInfo;
20
25
  #segments = [];
21
- #schemas = {};
26
+ #schemaSegments = {};
22
27
  #isWatching = false;
23
28
  #modulesWatcher = null;
24
29
  #segmentWatcher = null;
25
- #watchSegments = () => {
30
+ #onFirstTimeGenerate = null;
31
+ #schemaOut = null;
32
+ #devHttps;
33
+ #logLevel;
34
+ constructor({ schemaOut, devHttps, logLevel }) {
35
+ this.#schemaOut = schemaOut || null;
36
+ this.#devHttps = devHttps || false;
37
+ this.#logLevel = logLevel || 'info';
38
+ }
39
+ #watchSegments = (callback) => {
26
40
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
27
- const { cwd, log, config, apiDir } = this.#projectInfo;
28
- const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
29
- const apiDirAbsolutePath = path.join(cwd, apiDir);
41
+ const { cwd, log, config, apiDirAbsolutePath } = this.#projectInfo;
42
+ if (!apiDirAbsolutePath) {
43
+ throw new Error('Unable to watch segments. It looks like CWD is not a Next.js app.');
44
+ }
45
+ const schemaOutAbsolutePath = path.resolve(cwd, this.#schemaOut ?? config.schemaOutDir);
30
46
  const getSegmentName = (filePath) => path.relative(apiDirAbsolutePath, filePath).replace(segmentReg, '');
31
47
  log.debug(`Watching segments at ${apiDirAbsolutePath}`);
32
48
  this.#segmentWatcher = chokidar
@@ -40,7 +56,13 @@ export class VovkDev {
40
56
  const segmentName = getSegmentName(filePath);
41
57
  this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
42
58
  ? this.#segments
43
- : [...this.#segments, { routeFilePath: filePath, segmentName }];
59
+ : [
60
+ ...this.#segments,
61
+ {
62
+ routeFilePath: filePath,
63
+ segmentName,
64
+ },
65
+ ];
44
66
  log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
45
67
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
46
68
  void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
@@ -54,14 +76,22 @@ export class VovkDev {
54
76
  })
55
77
  .on('addDir', async (dirPath) => {
56
78
  log.debug(`Directory ${dirPath} has been added to segments folder`);
57
- this.#segments = await locateSegments(apiDirAbsolutePath);
79
+ this.#segments = await locateSegments({
80
+ dir: apiDirAbsolutePath,
81
+ config,
82
+ log: this.#projectInfo.log,
83
+ });
58
84
  for (const { segmentName } of this.#segments) {
59
85
  void this.#requestSchema(segmentName);
60
86
  }
61
87
  })
62
88
  .on('unlinkDir', async (dirPath) => {
63
89
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
64
- this.#segments = await locateSegments(apiDirAbsolutePath);
90
+ this.#segments = await locateSegments({
91
+ dir: apiDirAbsolutePath,
92
+ config,
93
+ log: this.#projectInfo.log,
94
+ });
65
95
  for (const { segmentName } of this.#segments) {
66
96
  void this.#requestSchema(segmentName);
67
97
  }
@@ -77,13 +107,14 @@ export class VovkDev {
77
107
  }
78
108
  })
79
109
  .on('ready', () => {
110
+ callback();
80
111
  log.debug('Segments watcher is ready');
81
112
  })
82
113
  .on('error', (error) => {
83
114
  log.error(`Error watching segments folder: ${error?.message ?? 'Unknown error'}`);
84
115
  });
85
116
  };
86
- #watchModules = () => {
117
+ #watchModules = (callback) => {
87
118
  const { config, cwd, log } = this.#projectInfo;
88
119
  const modulesDirAbsolutePath = path.join(cwd, config.modulesDir);
89
120
  log.debug(`Watching modules at ${modulesDirAbsolutePath}`);
@@ -115,30 +146,49 @@ export class VovkDev {
115
146
  }
116
147
  })
117
148
  .on('ready', () => {
149
+ callback();
118
150
  log.debug('Modules watcher is ready');
119
151
  })
120
152
  .on('error', (error) => {
121
153
  log.error(`Error watching modules folder: ${error?.message ?? 'Unknown error'}`);
122
154
  });
123
155
  };
124
- #watchConfig = () => {
156
+ #watchConfig = (callback) => {
125
157
  const { log, cwd } = this.#projectInfo;
126
158
  log.debug(`Watching config files`);
127
159
  let isInitial = true;
128
160
  let isReady = false;
129
161
  const handle = debounce(async () => {
130
- this.#projectInfo = await getProjectInfo();
131
- if (!isInitial) {
132
- log.info('Config file has been updated');
133
- isInitial = false;
134
- }
162
+ this.#projectInfo = await getProjectInfo({ logLevel: this.#logLevel });
163
+ const { config, apiDirAbsolutePath } = this.#projectInfo;
164
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
135
165
  await this.#modulesWatcher?.close();
136
166
  await this.#segmentWatcher?.close();
137
- this.#watchModules();
138
- this.#watchSegments();
167
+ await Promise.all([
168
+ new Promise((resolve) => this.#watchModules(() => resolve(0))),
169
+ new Promise((resolve) => this.#watchSegments(() => resolve(0))),
170
+ ]);
171
+ const schemaOutAbsolutePath = path.join(cwd, this.#schemaOut ?? this.#projectInfo.config.schemaOutDir);
172
+ if (isInitial) {
173
+ callback();
174
+ }
175
+ else {
176
+ log.info('Config file has been updated');
177
+ this.#generate();
178
+ }
179
+ await writeMetaJson(schemaOutAbsolutePath, this.#projectInfo);
180
+ isInitial = false;
139
181
  }, 1000);
140
182
  chokidar
141
- .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
183
+ // .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
184
+ .watch([
185
+ 'vovk.config.js',
186
+ 'vovk.config.mjs',
187
+ 'vovk.config.cjs',
188
+ '.config/vovk.config.js',
189
+ '.config/vovk.config.mjs',
190
+ '.config/vovk.config.cjs',
191
+ ], {
142
192
  persistent: true,
143
193
  cwd,
144
194
  ignoreInitial: false,
@@ -157,14 +207,13 @@ export class VovkDev {
157
207
  .on('error', (error) => log.error(`Error watching config files: ${error?.message ?? 'Unknown error'}`));
158
208
  void handle();
159
209
  };
160
- async #watch() {
210
+ async #watch(callback) {
161
211
  if (this.#isWatching)
162
212
  throw new Error('Already watching');
163
213
  const { log } = this.#projectInfo;
164
214
  log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
165
- await ensureClient(this.#projectInfo);
166
215
  // automatically watches segments and modules
167
- this.#watchConfig();
216
+ this.#watchConfig(callback);
168
217
  }
169
218
  #processControllerChange = async (filePath) => {
170
219
  const { log } = this.#projectInfo;
@@ -175,21 +224,17 @@ export class VovkDev {
175
224
  }
176
225
  const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
177
226
  const namesOfClasses = [...code.matchAll(nameOfClasReg)].map((match) => match[1]);
178
- const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options|worker)\b[^}]*}\s*from\s*['"]vovk['"]/;
227
+ const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
179
228
  if (importRegex.test(code) && namesOfClasses.length) {
180
229
  const affectedSegments = this.#segments.filter((s) => {
181
- const schema = this.#schemas[s.segmentName];
182
- if (!schema)
230
+ const segmentSchema = this.#schemaSegments[s.segmentName];
231
+ if (!segmentSchema)
183
232
  return false;
184
- const controllersByOriginalName = keyBy(schema.controllers, 'originalControllerName');
185
- const workersByOriginalName = keyBy(schema.workers, 'originalWorkerName');
186
- return namesOfClasses.some((name) => schema.controllers[name] ||
187
- schema.workers[name] ||
188
- controllersByOriginalName[name] ||
189
- workersByOriginalName[name]);
233
+ const controllersByOriginalName = keyBy(segmentSchema.controllers, 'originalControllerName');
234
+ return namesOfClasses.some((name) => segmentSchema.controllers[name] || controllersByOriginalName[name]);
190
235
  });
191
236
  if (affectedSegments.length) {
192
- 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))}`);
237
+ log.debug(`A file with controller ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${JSON.stringify(affectedSegments.map((s) => s.segmentName))}`);
193
238
  for (const segment of affectedSegments) {
194
239
  await this.#requestSchema(segment.segmentName);
195
240
  }
@@ -199,50 +244,75 @@ export class VovkDev {
199
244
  }
200
245
  }
201
246
  else {
202
- log.debug(`The file does not contain any controller or worker`);
247
+ log.debug(`The file ${filePath} does not contain any controller`);
203
248
  }
204
249
  };
205
250
  #requestSchema = debounceWithArgs(async (segmentName) => {
206
- const { apiEntryPoint, log, port, config } = this.#projectInfo;
207
- const { devHttps } = config;
208
- const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
251
+ const { apiRoot, log, port, config } = this.#projectInfo;
252
+ const devHttps = this.#devHttps ?? config.devHttps;
253
+ const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
209
254
  log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
210
- const resp = await fetch(endpoint);
211
- if (resp.status !== 200) {
212
- const probableCause = {
213
- 404: 'The segment did not compile or config.origin is wrong.',
214
- }[resp.status];
215
- log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
216
- return;
217
- }
218
- let schema = null;
219
255
  try {
220
- ({ schema } = (await resp.json()));
256
+ const resp = await fetch(endpoint);
257
+ const text = await resp.text();
258
+ const json = text ? JSON.parse(text) : null;
259
+ if (resp.status !== 200) {
260
+ const probableCause = {
261
+ 404: 'The segment did not compile or config.origin is wrong.',
262
+ }[resp.status];
263
+ log.warn(`Schema request to ${chalkHighlightThing(endpoint)} for ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}. Response text will be logged below on "debug" level.`);
264
+ log.debug(`Response from ${formatLoggedSegmentName(segmentName)}: ${text}`);
265
+ return { isError: true };
266
+ }
267
+ let segmentSchema = null;
268
+ try {
269
+ ({ schema: segmentSchema } = json);
270
+ }
271
+ catch (error) {
272
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error?.message}`);
273
+ }
274
+ await this.#handleSegmentSchema(segmentName, segmentSchema);
221
275
  }
222
276
  catch (error) {
223
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
277
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error?.message}`);
278
+ return { isError: true };
224
279
  }
225
- await this.#handleSchema(schema);
280
+ return { isError: false };
226
281
  }, 500);
227
- async #handleSchema(schema) {
282
+ #generate = debounce(() => {
283
+ const fullSchema = {
284
+ $schema: VovkSchemaIdEnum.SCHEMA,
285
+ segments: this.#schemaSegments,
286
+ meta: getMetaSchema({
287
+ config: this.#projectInfo.config,
288
+ useEmitConfig: false,
289
+ }),
290
+ };
291
+ return generate({
292
+ projectInfo: this.#projectInfo,
293
+ fullSchema,
294
+ locatedSegments: this.#segments,
295
+ }).then(this.#onFirstTimeGenerate);
296
+ }, 1000);
297
+ async #handleSegmentSchema(segmentName, segmentSchema) {
228
298
  const { log, config, cwd } = this.#projectInfo;
229
- if (!schema) {
230
- log.warn('Segment schema is null');
299
+ if (!segmentSchema) {
300
+ log.warn(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} schema is null`);
231
301
  return;
232
302
  }
233
- log.debug(`Handling received schema from ${formatLoggedSegmentName(schema.segmentName)}`);
234
- const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
235
- const segment = this.#segments.find((s) => s.segmentName === schema.segmentName);
303
+ log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
304
+ const schemaOutAbsolutePath = path.resolve(cwd, this.#schemaOut ?? config.schemaOutDir);
305
+ const segment = this.#segments.find((s) => s.segmentName === segmentName);
236
306
  if (!segment) {
237
- log.warn(`Segment "${schema.segmentName}" not found`);
307
+ log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
238
308
  return;
239
309
  }
240
- this.#schemas[schema.segmentName] = schema;
241
- if (schema.emitSchema) {
310
+ this.#schemaSegments[segmentName] = segmentSchema;
311
+ if (segmentSchema.emitSchema) {
242
312
  const now = Date.now();
243
- const { diffResult } = await writeOneSchemaFile({
313
+ const { diffResult } = await writeOneSegmentSchemaFile({
244
314
  schemaOutAbsolutePath,
245
- schema,
315
+ segmentSchema,
246
316
  skipIfExists: false,
247
317
  });
248
318
  const timeTook = Date.now() - now;
@@ -251,18 +321,27 @@ export class VovkDev {
251
321
  log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
252
322
  }
253
323
  }
254
- else if (schema && (!isEmpty(schema.controllers) || !isEmpty(schema.workers))) {
255
- log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but emitSchema is false`);
324
+ else if (segmentSchema && !isEmpty(segmentSchema.controllers)) {
325
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
256
326
  }
257
- if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
327
+ if (this.#segments.every((s) => this.#schemaSegments[s.segmentName])) {
258
328
  log.debug(`All segments with "emitSchema" have schema.`);
259
- await generateClient(this.#projectInfo, this.#segments, this.#schemas);
329
+ this.#generate();
260
330
  }
261
331
  }
262
- async start() {
263
- this.#projectInfo = await getProjectInfo();
264
- const { log, config, cwd, apiDir } = this.#projectInfo;
265
- if (config.devHttps) {
332
+ async start({ exit }) {
333
+ const now = Date.now();
334
+ this.#projectInfo = await getProjectInfo({ logLevel: this.#logLevel });
335
+ const { log, config, cwd, apiDirAbsolutePath } = this.#projectInfo;
336
+ this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
337
+ log.info('Starting...');
338
+ if (exit) {
339
+ this.#onFirstTimeGenerate = once(() => {
340
+ log.info('The schemas and the RPC client have been generated. Exiting...');
341
+ });
342
+ }
343
+ const devHttps = this.#devHttps ?? config.devHttps;
344
+ if (devHttps) {
266
345
  const agent = new Agent({
267
346
  connect: {
268
347
  rejectUnauthorized: false,
@@ -276,20 +355,53 @@ export class VovkDev {
276
355
  process.on('unhandledRejection', (reason) => {
277
356
  log.error(`Unhandled Rejection: ${String(reason)}`);
278
357
  });
279
- const apiDirAbsolutePath = path.join(cwd, apiDir);
280
- const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
281
- this.#segments = await locateSegments(apiDirAbsolutePath);
282
- await debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
283
- // Request schema every segment in 5 seconds in order to update schema and start watching
358
+ const schemaOutAbsolutePath = path.resolve(cwd, this.#schemaOut ?? config.schemaOutDir);
359
+ const segmentNames = this.#segments.map((s) => s.segmentName);
360
+ await ensureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, segmentNames);
361
+ await ensureClient(this.#projectInfo, this.#segments);
362
+ const MAX_ATTEMPTS = 5;
363
+ const DELAY = 5000;
364
+ // Request schema every segment in 5 seconds in order to update schema on start
284
365
  setTimeout(() => {
285
366
  for (const { segmentName } of this.#segments) {
286
- void this.#requestSchema(segmentName);
367
+ let attempts = 0;
368
+ void this.#requestSchema(segmentName).then(({ isError }) => {
369
+ if (isError) {
370
+ const interval = setInterval(() => {
371
+ attempts++;
372
+ if (attempts >= MAX_ATTEMPTS) {
373
+ clearInterval(interval);
374
+ log.error(`Failed to request schema for ${formatLoggedSegmentName(segmentName)} after ${MAX_ATTEMPTS} attempts`);
375
+ return;
376
+ }
377
+ void this.#requestSchema(segmentName).then(({ isError: isError2 }) => {
378
+ if (!isError2) {
379
+ clearInterval(interval);
380
+ log.info(`Requested schema for ${formatLoggedSegmentName(segmentName)} after ${attempts} attempt${attempts === 1 ? '' : 's'}`);
381
+ }
382
+ });
383
+ }, DELAY);
384
+ }
385
+ });
287
386
  }
288
- this.#watch();
289
- }, 5000);
387
+ }, DELAY);
388
+ if (!exit) {
389
+ this.#watch(() => {
390
+ log.info(`Ready in ${Date.now() - now}ms. Making initial requests for schemas in a moment...`);
391
+ });
392
+ }
393
+ else {
394
+ log.info(`Ready in ${Date.now() - now}ms. Making requests for schemas in a moment...`);
395
+ }
290
396
  }
291
397
  }
292
398
  const env = process.env;
293
399
  if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
294
- void new VovkDev().start();
400
+ void new VovkDev({
401
+ schemaOut: env.__VOVK_SCHEMA_OUT_FLAG__ || undefined,
402
+ devHttps: env.__VOVK_DEV_HTTPS_FLAG__ === 'true',
403
+ logLevel: env.__VOVK_LOG_LEVEL__,
404
+ }).start({
405
+ exit: env.__VOVK_EXIT__ === 'true',
406
+ });
295
407
  }
@@ -1,3 +1,3 @@
1
- import type { DiffResult } from './diffSchema.mjs';
1
+ import type { DiffResult } from './diffSegmentSchema.mjs';
2
2
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
3
  export default function logDiffResult(segmentName: string, diffResult: DiffResult, projectInfo: ProjectInfo): void;
@@ -3,23 +3,6 @@ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
3
3
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
4
4
  export default function logDiffResult(segmentName, diffResult, projectInfo) {
5
5
  const diffNormalized = [];
6
- diffResult.workers.added.forEach((name) => {
7
- diffNormalized.push({ what: 'worker', type: 'added', name });
8
- });
9
- diffResult.workers.removed.forEach((name) => {
10
- diffNormalized.push({ what: 'worker', type: 'removed', name });
11
- });
12
- diffResult.workers.handlers.forEach((handler) => {
13
- handler.added.forEach((name) => {
14
- diffNormalized.push({ what: 'workerHandler', type: 'added', name: `${handler.nameOfClass}.${name}` });
15
- });
16
- handler.removed.forEach((name) => {
17
- diffNormalized.push({ what: 'workerHandler', type: 'removed', name: `${handler.nameOfClass}.${name}` });
18
- });
19
- handler.changed.forEach((name) => {
20
- diffNormalized.push({ what: 'workerHandler', type: 'changed', name: `${handler.nameOfClass}.${name}` });
21
- });
22
- });
23
6
  diffResult.controllers.added.forEach((name) => {
24
7
  diffNormalized.push({ what: 'controller', type: 'added', name });
25
8
  });
@@ -37,52 +20,32 @@ export default function logDiffResult(segmentName, diffResult, projectInfo) {
37
20
  diffNormalized.push({ what: 'controllerHandler', type: 'changed', name: `${handler.nameOfClass}.${name}` });
38
21
  });
39
22
  });
40
- const LIMIT = diffNormalized.length < 12 ? diffNormalized.length : 10;
23
+ const LIMIT = diffNormalized.length < 17 ? diffNormalized.length : 15;
41
24
  const addedText = chalk.green('added');
42
25
  const removedText = chalk.red('removed');
43
26
  const changedText = chalk.cyan('changed');
44
27
  for (const diffNormalizedItem of diffNormalized.slice(0, LIMIT)) {
45
28
  switch (diffNormalizedItem.what) {
46
- case 'worker':
47
- switch (diffNormalizedItem.type) {
48
- case 'added':
49
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
50
- break;
51
- case 'removed':
52
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
53
- break;
54
- }
55
- break;
56
29
  case 'controller':
57
30
  switch (diffNormalizedItem.type) {
58
31
  case 'added':
59
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
60
- break;
61
- case 'removed':
62
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
63
- break;
64
- }
65
- break;
66
- case 'workerHandler':
67
- switch (diffNormalizedItem.type) {
68
- case 'added':
69
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
32
+ projectInfo.log.info(`Schema for RPC module ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
70
33
  break;
71
34
  case 'removed':
72
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
35
+ projectInfo.log.info(`Schema for RPC module ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
73
36
  break;
74
37
  }
75
38
  break;
76
39
  case 'controllerHandler':
77
40
  switch (diffNormalizedItem.type) {
78
41
  case 'added':
79
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
42
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
80
43
  break;
81
44
  case 'removed':
82
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
45
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
83
46
  break;
84
47
  case 'changed':
85
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${changedText} at ${formatLoggedSegmentName(segmentName)}`);
48
+ projectInfo.log.info(`Schema for RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${changedText} at ${formatLoggedSegmentName(segmentName)}`);
86
49
  break;
87
50
  }
88
51
  break;
@@ -0,0 +1,2 @@
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function writeMetaJson(schemaOutAbsolutePath: string, projectInfo: ProjectInfo): Promise<void>;
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { META_FILE_NAME } from './writeOneSegmentSchemaFile.mjs';
4
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
5
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
6
+ export default async function writeMetaJson(schemaOutAbsolutePath, projectInfo) {
7
+ const metaJsonPath = path.join(schemaOutAbsolutePath, META_FILE_NAME + '.json');
8
+ const metaStr = JSON.stringify(getMetaSchema({
9
+ config: projectInfo.config,
10
+ useEmitConfig: true,
11
+ }), null, 2);
12
+ const existingStr = await fs.readFile(metaJsonPath, 'utf-8').catch(() => null);
13
+ if (existingStr !== metaStr) {
14
+ await fs.writeFile(metaJsonPath, metaStr);
15
+ projectInfo?.log.info(`Meta JSON is written to ${chalkHighlightThing(metaJsonPath)}`);
16
+ }
17
+ else {
18
+ projectInfo?.log.debug(`Meta JSON is up to date at ${chalkHighlightThing(metaJsonPath)}`);
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { VovkSegmentSchema } from 'vovk';
2
+ import { type DiffResult } from './diffSegmentSchema.mjs';
3
+ export declare const ROOT_SEGMENT_FILE_NAME = "root";
4
+ export declare const META_FILE_NAME = "_meta";
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
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
- import diffSchema from './diffSchema.mjs';
3
+ import diffSegmentSchema from './diffSegmentSchema.mjs';
4
4
  import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
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`);
5
+ export const ROOT_SEGMENT_FILE_NAME = 'root';
6
+ export const META_FILE_NAME = '_meta';
7
+ export default async function writeOneSegmentSchemaFile({ schemaOutAbsolutePath, segmentSchema, skipIfExists = false, }) {
8
+ const segmentPath = path.join(schemaOutAbsolutePath, `${segmentSchema.segmentName || ROOT_SEGMENT_FILE_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,3 @@
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ import type { Segment } from '../locateSegments.mjs';
3
+ export default function ensureClient(projectInfo: ProjectInfo, locatedSegments: Segment[]): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { VovkSchemaIdEnum } from 'vovk';
2
+ import { generate } from './generate.mjs';
3
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
4
+ const getEmptySegmentRecordSchema = (segmentNames) => {
5
+ const result = {};
6
+ for (const segmentName of segmentNames) {
7
+ result[segmentName] = {
8
+ $schema: VovkSchemaIdEnum.SEGMENT,
9
+ segmentName,
10
+ segmentType: 'segment',
11
+ emitSchema: false,
12
+ controllers: {},
13
+ };
14
+ }
15
+ return result;
16
+ };
17
+ export default async function ensureClient(projectInfo, locatedSegments) {
18
+ return generate({
19
+ isEnsuringClient: true,
20
+ projectInfo,
21
+ fullSchema: {
22
+ $schema: VovkSchemaIdEnum.SCHEMA,
23
+ segments: getEmptySegmentRecordSchema(locatedSegments.map(({ segmentName }) => segmentName)),
24
+ meta: getMetaSchema({ config: projectInfo.config, useEmitConfig: false }),
25
+ },
26
+ locatedSegments,
27
+ });
28
+ }
@@ -0,0 +1,13 @@
1
+ import { type VovkSchema } from 'vovk';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ import type { GenerateOptions } from '../types.mjs';
4
+ import type { Segment } from '../locateSegments.mjs';
5
+ export declare function generate({ isEnsuringClient, isBundle, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, }: {
6
+ isEnsuringClient?: boolean;
7
+ isBundle?: boolean;
8
+ projectInfo: ProjectInfo;
9
+ forceNothingWrittenLog?: boolean;
10
+ fullSchema: VovkSchema;
11
+ locatedSegments: Segment[];
12
+ cliGenerateOptions?: GenerateOptions;
13
+ }): Promise<void>;