vovk-cli 0.0.1-draft.11 → 0.0.1-draft.112
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +29 -1
- package/client-templates/main/main.cjs.ejs +14 -0
- package/client-templates/main/main.d.cts.ejs +14 -0
- package/client-templates/module/module.d.mts.ejs +14 -0
- package/client-templates/module/module.mjs.ejs +20 -0
- package/client-templates/python/__init__.py +276 -0
- package/client-templates/ts/index.ts.ejs +23 -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 +53 -26
- package/dist/dev/index.d.mts +6 -0
- package/dist/{watcher → dev}/index.mjs +150 -77
- 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 +31 -0
- package/dist/generate/getClientTemplates.d.mts +16 -0
- package/dist/generate/getClientTemplates.mjs +42 -0
- package/dist/generate/index.d.mts +13 -0
- package/dist/generate/index.mjs +105 -0
- package/dist/getProjectInfo/getConfig.d.mts +3 -3
- package/dist/getProjectInfo/getConfig.mjs +22 -5
- package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +2 -2
- package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
- package/dist/getProjectInfo/getUserConfig.d.mts +1 -1
- package/dist/getProjectInfo/getUserConfig.mjs +3 -1
- package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
- package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
- package/dist/getProjectInfo/index.d.mts +14 -5
- package/dist/getProjectInfo/index.mjs +21 -13
- 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 +12 -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 +13 -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 +39 -34
- package/dist/new/newSegment.mjs +8 -6
- package/dist/new/render.d.mts +5 -2
- package/dist/new/render.mjs +25 -13
- package/dist/postinstall.mjs +16 -19
- package/dist/types.d.mts +35 -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/package.json +23 -18
- 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/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
|
|
4
|
+
import { Agent, setGlobalDispatcher } from 'undici';
|
|
5
|
+
import keyBy from 'lodash/keyBy.js';
|
|
6
|
+
import capitalize from 'lodash/capitalize.js';
|
|
7
|
+
import debounce from 'lodash/debounce.js';
|
|
8
|
+
import once from 'lodash/once.js';
|
|
5
9
|
import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
|
|
6
|
-
import
|
|
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,14 @@ export class VovkCLIWatcher {
|
|
|
156
188
|
.on('error', (error) => log.error(`Error watching config files: ${error?.message ?? 'Unknown error'}`));
|
|
157
189
|
void handle();
|
|
158
190
|
};
|
|
159
|
-
#watch() {
|
|
191
|
+
async #watch(callback) {
|
|
160
192
|
if (this.#isWatching)
|
|
161
193
|
throw new Error('Already watching');
|
|
162
194
|
const { log } = this.#projectInfo;
|
|
163
195
|
log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
|
|
196
|
+
await ensureClient(this.#projectInfo);
|
|
164
197
|
// automatically watches segments and modules
|
|
165
|
-
this.#watchConfig();
|
|
198
|
+
this.#watchConfig(callback);
|
|
166
199
|
}
|
|
167
200
|
#processControllerChange = async (filePath) => {
|
|
168
201
|
const { log } = this.#projectInfo;
|
|
@@ -173,72 +206,78 @@ export class VovkCLIWatcher {
|
|
|
173
206
|
}
|
|
174
207
|
const nameOfClasReg = /\bclass\s+([A-Za-z_]\w*)(?:\s*<[^>]*>)?\s*\{/g;
|
|
175
208
|
const namesOfClasses = [...code.matchAll(nameOfClasReg)].map((match) => match[1]);
|
|
176
|
-
const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options
|
|
209
|
+
const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
|
|
177
210
|
if (importRegex.test(code) && namesOfClasses.length) {
|
|
178
211
|
const affectedSegments = this.#segments.filter((s) => {
|
|
179
|
-
const
|
|
180
|
-
if (!
|
|
212
|
+
const segmentSchema = this.#fullSchema.segments[s.segmentName];
|
|
213
|
+
if (!segmentSchema)
|
|
181
214
|
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]);
|
|
215
|
+
const controllersByOriginalName = keyBy(segmentSchema.controllers, 'originalControllerName');
|
|
216
|
+
return namesOfClasses.some((name) => segmentSchema.controllers[name] || controllersByOriginalName[name]);
|
|
188
217
|
});
|
|
189
218
|
if (affectedSegments.length) {
|
|
190
|
-
log.debug(`A file with controller
|
|
219
|
+
log.debug(`A file with controller ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${JSON.stringify(affectedSegments.map((s) => s.segmentName))}`);
|
|
191
220
|
for (const segment of affectedSegments) {
|
|
192
221
|
await this.#requestSchema(segment.segmentName);
|
|
193
222
|
}
|
|
194
223
|
}
|
|
224
|
+
else {
|
|
225
|
+
log.debug(`The class ${namesOfClasses.join(', ')} does not belong to any segment`);
|
|
226
|
+
}
|
|
195
227
|
}
|
|
196
228
|
else {
|
|
197
|
-
log.debug(`The file does not contain any controller
|
|
229
|
+
log.debug(`The file does not contain any controller`);
|
|
198
230
|
}
|
|
199
231
|
};
|
|
200
232
|
#requestSchema = debounceWithArgs(async (segmentName) => {
|
|
201
|
-
const {
|
|
233
|
+
const { apiRoot, log, port, config } = this.#projectInfo;
|
|
202
234
|
const { devHttps } = config;
|
|
203
|
-
const endpoint = `${
|
|
235
|
+
const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
|
|
204
236
|
log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
|
|
205
|
-
const resp = await fetch(endpoint);
|
|
206
|
-
if (resp.status !== 200) {
|
|
207
|
-
const probableCause = {
|
|
208
|
-
404: 'The segment is not compiled.',
|
|
209
|
-
500: 'Syntax error in one of controllers.',
|
|
210
|
-
}[resp.status];
|
|
211
|
-
log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
let schema = null;
|
|
215
237
|
try {
|
|
216
|
-
|
|
238
|
+
const resp = await fetch(endpoint);
|
|
239
|
+
if (resp.status !== 200) {
|
|
240
|
+
const probableCause = {
|
|
241
|
+
404: 'The segment did not compile or config.origin is wrong.',
|
|
242
|
+
}[resp.status];
|
|
243
|
+
log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
|
|
244
|
+
return { isError: true };
|
|
245
|
+
}
|
|
246
|
+
let segmentSchema = null;
|
|
247
|
+
try {
|
|
248
|
+
({ schema: segmentSchema } = (await resp.json()));
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
|
|
252
|
+
}
|
|
253
|
+
await this.#handleSegmentSchema(segmentName, segmentSchema);
|
|
217
254
|
}
|
|
218
255
|
catch (error) {
|
|
219
|
-
log.error(`Error
|
|
256
|
+
log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
|
|
257
|
+
return { isError: true };
|
|
220
258
|
}
|
|
221
|
-
|
|
259
|
+
return { isError: false };
|
|
222
260
|
}, 500);
|
|
223
|
-
|
|
261
|
+
#generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, fullSchema: this.#fullSchema }).then(this.#onFirstTimeGenerate), 1000);
|
|
262
|
+
async #handleSegmentSchema(segmentName, segmentSchema) {
|
|
224
263
|
const { log, config, cwd } = this.#projectInfo;
|
|
225
|
-
if (!
|
|
226
|
-
log.warn(
|
|
264
|
+
if (!segmentSchema) {
|
|
265
|
+
log.warn(`${formatLoggedSegmentName(segmentName)} schema is null`);
|
|
227
266
|
return;
|
|
228
267
|
}
|
|
229
|
-
log.debug(`Handling received schema from ${formatLoggedSegmentName(
|
|
268
|
+
log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
|
|
230
269
|
const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
|
|
231
|
-
const segment = this.#segments.find((s) => s.segmentName ===
|
|
270
|
+
const segment = this.#segments.find((s) => s.segmentName === segmentName);
|
|
232
271
|
if (!segment) {
|
|
233
|
-
log.warn(
|
|
272
|
+
log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
|
|
234
273
|
return;
|
|
235
274
|
}
|
|
236
|
-
this.#
|
|
237
|
-
if (
|
|
275
|
+
this.#fullSchema.segments[segmentName] = segmentSchema;
|
|
276
|
+
if (segmentSchema.emitSchema) {
|
|
238
277
|
const now = Date.now();
|
|
239
|
-
const { diffResult } = await
|
|
278
|
+
const { diffResult } = await writeOneSegmentSchemaFile({
|
|
240
279
|
schemaOutAbsolutePath,
|
|
241
|
-
|
|
280
|
+
segmentSchema,
|
|
242
281
|
skipIfExists: false,
|
|
243
282
|
});
|
|
244
283
|
const timeTook = Date.now() - now;
|
|
@@ -247,17 +286,24 @@ export class VovkCLIWatcher {
|
|
|
247
286
|
log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
|
|
248
287
|
}
|
|
249
288
|
}
|
|
250
|
-
else if (
|
|
251
|
-
log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but emitSchema is false`);
|
|
289
|
+
else if (segmentSchema && !isSegmentSchemaEmpty(segmentSchema)) {
|
|
290
|
+
log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
|
|
252
291
|
}
|
|
253
|
-
if (this.#segments.every((s) => this.#
|
|
292
|
+
if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
|
|
254
293
|
log.debug(`All segments with "emitSchema" have schema.`);
|
|
255
|
-
|
|
294
|
+
this.#generate();
|
|
256
295
|
}
|
|
257
296
|
}
|
|
258
|
-
async start({
|
|
259
|
-
|
|
297
|
+
async start({ exit }) {
|
|
298
|
+
const now = Date.now();
|
|
299
|
+
this.#projectInfo = await getProjectInfo();
|
|
260
300
|
const { log, config, cwd, apiDir } = this.#projectInfo;
|
|
301
|
+
log.info('Starting...');
|
|
302
|
+
if (exit) {
|
|
303
|
+
this.#onFirstTimeGenerate = once(() => {
|
|
304
|
+
log.info('The schemas and the RPC client have been generated. Exiting...');
|
|
305
|
+
});
|
|
306
|
+
}
|
|
261
307
|
if (config.devHttps) {
|
|
262
308
|
const agent = new Agent({
|
|
263
309
|
connect: {
|
|
@@ -274,18 +320,45 @@ export class VovkCLIWatcher {
|
|
|
274
320
|
});
|
|
275
321
|
const apiDirAbsolutePath = path.join(cwd, apiDir);
|
|
276
322
|
const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
|
|
277
|
-
this.#segments = await locateSegments(apiDirAbsolutePath);
|
|
323
|
+
this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
|
|
278
324
|
await debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
|
|
279
|
-
|
|
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,31 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import getClientTemplates from './getClientTemplates.mjs';
|
|
3
|
+
import uniq from 'lodash/uniq.js';
|
|
4
|
+
import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
export default async function ensureClient({ config, cwd, log }) {
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
const { clientOutDirAbsolutePath, templateFiles } = getClientTemplates({
|
|
9
|
+
config,
|
|
10
|
+
cwd,
|
|
11
|
+
generateFrom: config.generateFrom,
|
|
12
|
+
});
|
|
13
|
+
let usedTemplateNames = [];
|
|
14
|
+
const text = `// auto-generated ${new Date().toISOString()}
|
|
15
|
+
// This is a temporary placeholder to avoid compilation errors if client is imported before it's generated.
|
|
16
|
+
// If you still see this text, the client is not generated yet because of an unknown problem.
|
|
17
|
+
// Feel free to report an issue at https://github.com/finom/vovk/issues`;
|
|
18
|
+
for (const { outPath, templateName } of templateFiles) {
|
|
19
|
+
const existing = await fs.readFile(outPath, 'utf-8').catch(() => null);
|
|
20
|
+
if (!existing) {
|
|
21
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
22
|
+
await fs.writeFile(outPath, outPath.endsWith('.py') ? text.replace(/\/\//g, '#') : text);
|
|
23
|
+
usedTemplateNames.push(templateName);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
usedTemplateNames = uniq(usedTemplateNames);
|
|
27
|
+
if (usedTemplateNames.length) {
|
|
28
|
+
log.info(`Placeholder client files from template${usedTemplateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(usedTemplateNames.map((s) => `"${s}"`).join(', '))} are generated at ${clientOutDirAbsolutePath} in ${Date.now() - now}ms`);
|
|
29
|
+
}
|
|
30
|
+
return { written: !!usedTemplateNames.length };
|
|
31
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { VovkStrictConfig } from 'vovk';
|
|
2
|
+
interface ClientTemplate {
|
|
3
|
+
templateName: string;
|
|
4
|
+
templatePath: string;
|
|
5
|
+
outPath: string;
|
|
6
|
+
emitFullSchema?: string | boolean;
|
|
7
|
+
}
|
|
8
|
+
export default function getClientTemplates({ config, cwd, generateFrom, }: {
|
|
9
|
+
config: VovkStrictConfig;
|
|
10
|
+
cwd: string;
|
|
11
|
+
generateFrom?: VovkStrictConfig['generateFrom'];
|
|
12
|
+
}): {
|
|
13
|
+
clientOutDirAbsolutePath: string;
|
|
14
|
+
templateFiles: ClientTemplate[];
|
|
15
|
+
};
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
export default function getClientTemplates({ config, cwd, generateFrom = [], }) {
|
|
3
|
+
const templatesDir = path.join(import.meta.dirname, '../..', 'client-templates');
|
|
4
|
+
const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
|
|
5
|
+
const mapper = (dir) => (name) => ({
|
|
6
|
+
templateName: dir,
|
|
7
|
+
templatePath: path.resolve(templatesDir, dir, name),
|
|
8
|
+
outPath: path.join(clientOutDirAbsolutePath, name.replace('.ejs', '')),
|
|
9
|
+
});
|
|
10
|
+
const builtInTemplatesMap = {
|
|
11
|
+
ts: ['index.ts.ejs'].map(mapper('ts')),
|
|
12
|
+
main: ['main.cjs.ejs', 'main.d.cts.ejs'].map(mapper('main')),
|
|
13
|
+
module: ['module.mjs.ejs', 'module.d.mts.ejs'].map(mapper('module')),
|
|
14
|
+
python: ['__init__.py'].map(mapper('python')),
|
|
15
|
+
};
|
|
16
|
+
const templateFiles = (generateFrom ?? config.generateFrom).reduce((acc, template) => {
|
|
17
|
+
if (typeof template === 'string') {
|
|
18
|
+
if (template in builtInTemplatesMap) {
|
|
19
|
+
return [...acc, ...builtInTemplatesMap[template]];
|
|
20
|
+
}
|
|
21
|
+
return [
|
|
22
|
+
...acc,
|
|
23
|
+
{
|
|
24
|
+
templateName: template,
|
|
25
|
+
templatePath: path.resolve(cwd, template),
|
|
26
|
+
outPath: path.join(clientOutDirAbsolutePath, path.basename(template).replace('.ejs', '')),
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
const outDirAbsolutePath = template.outDir ? path.resolve(cwd, template.outDir) : clientOutDirAbsolutePath;
|
|
31
|
+
return [
|
|
32
|
+
...acc,
|
|
33
|
+
{
|
|
34
|
+
templateName: template.templateName ?? template.templatePath,
|
|
35
|
+
templatePath: path.resolve(cwd, template.templatePath),
|
|
36
|
+
outPath: path.join(outDirAbsolutePath, path.basename(template.templatePath).replace('.ejs', '')),
|
|
37
|
+
fullSchema: template.fullSchema,
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
}, []);
|
|
41
|
+
return { clientOutDirAbsolutePath, templateFiles };
|
|
42
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { VovkFullSchema } from 'vovk';
|
|
2
|
+
import type { ProjectInfo } from '../getProjectInfo/index.mjs';
|
|
3
|
+
import type { Segment } from '../locateSegments.mjs';
|
|
4
|
+
import type { GenerateOptions } from '../types.mjs';
|
|
5
|
+
export default function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }: {
|
|
6
|
+
projectInfo: ProjectInfo;
|
|
7
|
+
segments: Segment[];
|
|
8
|
+
forceNothingWrittenLog?: boolean;
|
|
9
|
+
fullSchema: VovkFullSchema;
|
|
10
|
+
} & Pick<GenerateOptions, 'templates' | 'prettify' | 'emitFullSchema'>): Promise<{
|
|
11
|
+
written: boolean;
|
|
12
|
+
path: string;
|
|
13
|
+
}>;
|