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