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