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.
- package/LICENSE +1 -1
- package/README.md +29 -1
- package/client-templates/fullSchema/fullSchema.cjs.ejs +13 -0
- package/client-templates/fullSchema/fullSchema.d.cts.ejs +11 -0
- package/client-templates/main/main.cjs.ejs +15 -0
- package/client-templates/main/main.d.cts.ejs +15 -0
- package/client-templates/module/module.d.mts.ejs +15 -0
- package/client-templates/module/module.mjs.ejs +21 -0
- package/client-templates/ts/index.ts.ejs +24 -0
- package/dist/dev/diffSegmentSchema.d.mts +36 -0
- package/dist/{watcher/diffSchema.mjs → dev/diffSegmentSchema.mjs} +4 -12
- package/dist/{watcher → dev}/ensureSchemaFiles.d.mts +3 -0
- package/dist/{watcher → dev}/ensureSchemaFiles.mjs +14 -32
- package/dist/dev/index.d.mts +6 -0
- package/dist/{watcher → dev}/index.mjs +152 -79
- package/dist/dev/isSegmentSchemaEmpty.d.mts +2 -0
- package/dist/dev/isSegmentSchemaEmpty.mjs +4 -0
- package/dist/dev/logDiffResult.d.mts +3 -0
- package/dist/dev/logDiffResult.mjs +57 -0
- package/dist/dev/writeConfigJson.d.mts +2 -0
- package/dist/dev/writeConfigJson.mjs +15 -0
- package/dist/dev/writeOneSegmentSchemaFile.d.mts +12 -0
- package/dist/{watcher/writeOneSchemaFile.mjs → dev/writeOneSegmentSchemaFile.mjs} +11 -7
- package/dist/generate/ensureClient.d.mts +4 -0
- package/dist/generate/ensureClient.mjs +41 -0
- package/dist/generate/getClientTemplates.d.mts +24 -0
- package/dist/generate/getClientTemplates.mjs +86 -0
- package/dist/generate/getFullSchemaFromJSON.d.mts +3 -0
- package/dist/generate/getFullSchemaFromJSON.mjs +64 -0
- package/dist/generate/index.d.mts +13 -0
- package/dist/generate/index.mjs +115 -0
- package/dist/getProjectInfo/getConfig.d.mts +5 -4
- package/dist/getProjectInfo/getConfig.mjs +26 -7
- package/dist/getProjectInfo/getConfigAbsolutePaths.d.mts +2 -1
- package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +6 -3
- package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
- package/dist/getProjectInfo/getUserConfig.d.mts +3 -2
- package/dist/getProjectInfo/getUserConfig.mjs +5 -3
- package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
- package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
- package/dist/getProjectInfo/index.d.mts +14 -6
- package/dist/getProjectInfo/index.mjs +23 -15
- package/dist/index.d.mts +0 -28
- package/dist/index.mjs +60 -64
- package/dist/init/checkTSConfigForExperimentalDecorators.mjs +2 -2
- package/dist/init/createConfig.d.mts +3 -4
- package/dist/init/createConfig.mjs +14 -10
- package/dist/init/getTemplateFilesFromPackage.d.mts +2 -1
- package/dist/init/getTemplateFilesFromPackage.mjs +4 -5
- package/dist/init/index.d.mts +2 -3
- package/dist/init/index.mjs +31 -88
- package/dist/init/installDependencies.d.mts +1 -1
- package/dist/init/installDependencies.mjs +1 -1
- package/dist/init/logUpdateDependenciesError.d.mts +2 -2
- package/dist/init/logUpdateDependenciesError.mjs +3 -3
- package/dist/init/updateDependenciesWithoutInstalling.d.mts +3 -2
- package/dist/init/updateDependenciesWithoutInstalling.mjs +7 -9
- package/dist/init/updateNPMScripts.d.mts +3 -1
- package/dist/init/updateNPMScripts.mjs +10 -7
- package/dist/init/updateTypeScriptConfig.mjs +2 -2
- package/dist/initProgram.d.mts +2 -0
- package/dist/initProgram.mjs +21 -0
- package/dist/locateSegments.d.mts +7 -1
- package/dist/locateSegments.mjs +9 -6
- package/dist/new/addClassToSegmentCode.d.mts +1 -2
- package/dist/new/addClassToSegmentCode.mjs +5 -5
- package/dist/new/addCommonTerms.mjs +1 -0
- package/dist/new/index.d.mts +2 -2
- package/dist/new/index.mjs +3 -2
- package/dist/new/newModule.d.mts +3 -2
- package/dist/new/newModule.mjs +41 -37
- package/dist/new/newSegment.mjs +8 -6
- package/dist/new/render.d.mts +6 -3
- package/dist/new/render.mjs +25 -13
- package/dist/types.d.mts +36 -40
- package/dist/utils/debounceWithArgs.d.mts +2 -2
- package/dist/utils/debounceWithArgs.mjs +24 -9
- package/dist/utils/formatLoggedSegmentName.mjs +1 -1
- package/dist/utils/getAvailablePort.mjs +2 -1
- package/dist/utils/getFileSystemEntryType.mjs +1 -1
- package/dist/utils/resolveAbsoluteModulePath.d.mts +1 -0
- package/dist/utils/resolveAbsoluteModulePath.mjs +6 -0
- package/package.json +26 -21
- package/templates/controller.ejs +22 -23
- package/templates/service.ejs +13 -13
- package/dist/generateClient.d.mts +0 -7
- package/dist/generateClient.mjs +0 -97
- package/dist/postinstall.d.mts +0 -1
- package/dist/postinstall.mjs +0 -24
- package/dist/watcher/diffSchema.d.mts +0 -43
- package/dist/watcher/index.d.mts +0 -6
- package/dist/watcher/isMetadataEmpty.d.mts +0 -2
- package/dist/watcher/isMetadataEmpty.mjs +0 -4
- package/dist/watcher/logDiffResult.d.mts +0 -3
- package/dist/watcher/logDiffResult.mjs +0 -90
- package/dist/watcher/writeOneSchemaFile.d.mts +0 -11
- 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
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
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
|
|
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
|
|
15
|
-
import
|
|
16
|
-
|
|
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
|
-
#
|
|
23
|
+
#fullSchema = {
|
|
24
|
+
segments: {},
|
|
25
|
+
config: {},
|
|
26
|
+
};
|
|
21
27
|
#isWatching = false;
|
|
22
28
|
#modulesWatcher = null;
|
|
23
29
|
#segmentWatcher = null;
|
|
24
|
-
#
|
|
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
|
|
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
|
-
: [
|
|
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
|
|
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
|
-
|
|
137
|
-
|
|
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
|
|
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
|
|
180
|
-
if (!
|
|
211
|
+
const segmentSchema = this.#fullSchema.segments[s.segmentName];
|
|
212
|
+
if (!segmentSchema)
|
|
181
213
|
return false;
|
|
182
|
-
const controllersByOriginalName = keyBy(
|
|
183
|
-
|
|
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
|
|
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
|
|
228
|
+
log.debug(`The file ${filePath} does not contain any controller`);
|
|
198
229
|
}
|
|
199
230
|
};
|
|
200
231
|
#requestSchema = debounceWithArgs(async (segmentName) => {
|
|
201
|
-
const {
|
|
232
|
+
const { apiRoot, log, port, config } = this.#projectInfo;
|
|
202
233
|
const { devHttps } = config;
|
|
203
|
-
const endpoint = `${
|
|
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
|
-
|
|
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
|
|
255
|
+
log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
|
|
256
|
+
return { isError: true };
|
|
220
257
|
}
|
|
221
|
-
|
|
258
|
+
return { isError: false };
|
|
222
259
|
}, 500);
|
|
223
|
-
|
|
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 (!
|
|
226
|
-
log.warn(
|
|
263
|
+
if (!segmentSchema) {
|
|
264
|
+
log.warn(`${formatLoggedSegmentName(segmentName)} schema is null`);
|
|
227
265
|
return;
|
|
228
266
|
}
|
|
229
|
-
log.debug(`Handling received schema from ${formatLoggedSegmentName(
|
|
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 ===
|
|
269
|
+
const segment = this.#segments.find((s) => s.segmentName === segmentName);
|
|
232
270
|
if (!segment) {
|
|
233
|
-
log.warn(
|
|
271
|
+
log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
|
|
234
272
|
return;
|
|
235
273
|
}
|
|
236
|
-
this.#
|
|
237
|
-
if (
|
|
274
|
+
this.#fullSchema.segments[segmentName] = segmentSchema;
|
|
275
|
+
if (segmentSchema.emitSchema) {
|
|
238
276
|
const now = Date.now();
|
|
239
|
-
const { diffResult } = await
|
|
277
|
+
const { diffResult } = await writeOneSegmentSchemaFile({
|
|
240
278
|
schemaOutAbsolutePath,
|
|
241
|
-
|
|
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 (
|
|
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.#
|
|
291
|
+
if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
|
|
254
292
|
log.debug(`All segments with "emitSchema" have schema.`);
|
|
255
|
-
|
|
293
|
+
this.#generate();
|
|
256
294
|
}
|
|
257
295
|
}
|
|
258
|
-
async start({
|
|
259
|
-
|
|
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
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
|
|
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
|
|
363
|
+
void new VovkDev().start({ exit: env.__VOVK_EXIT__ === 'true' });
|
|
291
364
|
}
|
|
@@ -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,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
|
|
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
|
|
7
|
-
|
|
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(
|
|
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 {
|
|
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,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 {};
|