vovk-cli 0.0.1-draft.113 → 0.0.1-draft.115
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 +1 -1
- package/client-templates/fullSchema/fullSchema.cjs.ejs +13 -0
- package/client-templates/fullSchema/fullSchema.d.cts.ejs +11 -0
- package/client-templates/main/main.cjs.ejs +2 -1
- package/client-templates/main/main.d.cts.ejs +1 -0
- package/client-templates/module/module.d.mts.ejs +1 -0
- package/client-templates/module/module.mjs.ejs +2 -1
- package/client-templates/ts/index.ts.ejs +2 -1
- package/dist/dev/ensureSchemaFiles.mjs +0 -45
- package/dist/dev/index.mjs +4 -4
- package/dist/generate/getClientTemplates.mjs +10 -0
- package/dist/generate/index.mjs +5 -0
- package/dist/getProjectInfo/index.d.mts +0 -2
- package/dist/getProjectInfo/index.mjs +0 -4
- package/dist/index.mjs +3 -4
- package/dist/utils/getFullSchemaFromJSON.d.mts +3 -0
- package/dist/utils/getFullSchemaFromJSON.mjs +63 -0
- package/package.json +2 -2
- package/client-templates/python/__init__.py +0 -276
- package/dist/postinstall.d.mts +0 -1
- package/dist/postinstall.mjs +0 -21
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<source width="300" media="(prefers-color-scheme: light)" srcset="https://vovk.dev/vovk-logo.svg">
|
|
5
5
|
<img width="300" alt="vovk" src="https://vovk.dev/vovk-logo.svg">
|
|
6
6
|
</picture><br>
|
|
7
|
-
<strong>
|
|
7
|
+
<strong>RESTful + RPC = ♥️</strong>
|
|
8
8
|
</p>
|
|
9
9
|
|
|
10
10
|
<p align="center">
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%- `// auto-generated ${new Date().toISOString()}\n/* eslint-disable */` %>
|
|
2
|
+
const config = require('./<%= t.schemaOutDir %>/config.json');
|
|
3
|
+
const segments = {
|
|
4
|
+
<% Object.values(t.fullSchema.segments).forEach((segment) => { %>
|
|
5
|
+
'<%= segment.segmentName %>': require('./<%= t.schemaOutDir %>/<%= t.SEGMENTS_SCHEMA_DIR_NAME %>/<%= segment.segmentName || t.ROOT_SEGMENT_SCHEMA_NAME %>.json'),
|
|
6
|
+
<% }) %>
|
|
7
|
+
};
|
|
8
|
+
const fullSchema = {
|
|
9
|
+
config,
|
|
10
|
+
segments,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
module.exports.fullSchema = fullSchema;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<%- `// auto-generated ${new Date().toISOString()}\n/* eslint-disable */` %>
|
|
2
|
+
import { VovkStrictConfig, VovkSegmentSchema } from 'vovk';
|
|
3
|
+
|
|
4
|
+
export const fullSchema: {
|
|
5
|
+
config: Partial<VovkStrictConfig>;
|
|
6
|
+
segments: {
|
|
7
|
+
<% Object.values(t.fullSchema.segments).forEach((segment) => { %>
|
|
8
|
+
'<%= segment.segmentName %>': VovkSegmentSchema;
|
|
9
|
+
<% }) %>
|
|
10
|
+
};
|
|
11
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%- `// auto-generated ${new Date().toISOString()}\n/* eslint-disable */` %>
|
|
2
2
|
const { fetcher } = require('<%= t.imports.fetcher %>');
|
|
3
3
|
const { createRPC } = require('<%= t.imports.createRPC %>');
|
|
4
|
-
const fullSchema = require('
|
|
4
|
+
const { fullSchema } = require('./fullSchema.cjs');
|
|
5
5
|
const { validateOnClient = null } = <%- t.imports.validateOnClient ? `require('${t.imports.validateOnClient}')` : '{}'%>;
|
|
6
6
|
const apiRoot = '<%= t.apiRoot %>';
|
|
7
7
|
<% Object.values(t.fullSchema.segments).forEach((segment) => {
|
|
@@ -12,3 +12,4 @@ exports.<%= controllerName %> = createRPC(
|
|
|
12
12
|
);
|
|
13
13
|
<% })
|
|
14
14
|
}) %>
|
|
15
|
+
exports.fullSchema = fullSchema;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<%- `// auto-generated ${new Date().toISOString()}\n/* eslint-disable */` %>
|
|
2
2
|
import { fetcher } from '<%= t.imports.module.fetcher %>';
|
|
3
3
|
import { createRPC } from '<%= t.imports.module.createRPC %>';
|
|
4
|
-
import fullSchema from '
|
|
4
|
+
import { fullSchema } from './fullSchema.cjs';
|
|
5
5
|
<% if (t.imports.module.validateOnClient) { %>
|
|
6
6
|
import { validateOnClient } from '<%= t.imports.module.validateOnClient %>';
|
|
7
7
|
<% } else { %>
|
|
@@ -18,3 +18,4 @@ export const <%= controllerName %> = createRPC(
|
|
|
18
18
|
});
|
|
19
19
|
});
|
|
20
20
|
%>
|
|
21
|
+
export { fullSchema };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { VovkClientFetcher } from 'vovk';
|
|
3
3
|
import { fetcher } from '<%= t.imports.fetcher %>';
|
|
4
4
|
import { createRPC } from '<%= t.imports.createRPC %>';
|
|
5
|
-
import fullSchema from '
|
|
5
|
+
import { fullSchema } from './fullSchema.cjs';
|
|
6
6
|
<% Object.values(t.fullSchema.segments).forEach((segment, i) => { if(Object.keys(segment.controllers).length) { %>
|
|
7
7
|
import type { Controllers as Controllers<%= i %> } from "<%= t.segmentMeta[segment.segmentName].segmentImportPath %>";
|
|
8
8
|
<% }}) %>
|
|
@@ -21,3 +21,4 @@ export const <%= controllerName %> = createRPC<Controllers<%= i %>["<%= controll
|
|
|
21
21
|
);
|
|
22
22
|
<% }) %>
|
|
23
23
|
<% }) %>
|
|
24
|
+
export { fullSchema };
|
|
@@ -11,41 +11,6 @@ export default async function ensureSchemaFiles(projectInfo, schemaOutAbsolutePa
|
|
|
11
11
|
const now = Date.now();
|
|
12
12
|
let hasChanged = false;
|
|
13
13
|
const schemaJsonOutAbsolutePath = path.join(schemaOutAbsolutePath, SEGMENTS_SCHEMA_DIR_NAME);
|
|
14
|
-
const jsContent = `// auto-generated ${new Date().toISOString()}
|
|
15
|
-
module.exports.config = require('./config.json');
|
|
16
|
-
module.exports.segments = {
|
|
17
|
-
${segmentNames
|
|
18
|
-
.map((segmentName) => {
|
|
19
|
-
return ` '${segmentName}': require('./${SEGMENTS_SCHEMA_DIR_NAME}/${segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json'),`;
|
|
20
|
-
})
|
|
21
|
-
.join('\n')}
|
|
22
|
-
}`;
|
|
23
|
-
const dTsContent = `// auto-generated ${new Date().toISOString()}
|
|
24
|
-
import type { VovkSegmentSchema, VovkStrictConfig } from 'vovk';
|
|
25
|
-
declare const fullSchema: {
|
|
26
|
-
config: Partial<VovkStrictConfig>;
|
|
27
|
-
segments: {
|
|
28
|
-
${segmentNames.map((segmentName) => ` '${segmentName}': VovkSegmentSchema;`).join('\n')}
|
|
29
|
-
};
|
|
30
|
-
};
|
|
31
|
-
export default fullSchema;`;
|
|
32
|
-
const tsContent = `// auto-generated ${new Date().toISOString()}
|
|
33
|
-
import type { VovkSegmentSchema, VovkStrictConfig } from 'vovk';
|
|
34
|
-
import config from './config.json';
|
|
35
|
-
${segmentNames.map((segmentName, i) => `import segment${i} from './${SEGMENTS_SCHEMA_DIR_NAME}/${segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json';`).join('\n')}
|
|
36
|
-
const fullSchema = {
|
|
37
|
-
config: config as unknown as Partial<VovkStrictConfig>,
|
|
38
|
-
segments: {
|
|
39
|
-
${segmentNames.map((segmentName, i) => ` '${segmentName}': segment${i} as unknown as VovkSegmentSchema,`).join('\n')}
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
export default fullSchema;`;
|
|
43
|
-
const jsAbsolutePath = path.join(schemaOutAbsolutePath, 'main.cjs');
|
|
44
|
-
const dTsAbsolutePath = path.join(schemaOutAbsolutePath, 'main.d.cts');
|
|
45
|
-
const tsAbsolutePath = path.join(schemaOutAbsolutePath, 'index.ts');
|
|
46
|
-
const existingJs = await fs.readFile(jsAbsolutePath, 'utf-8').catch(() => null);
|
|
47
|
-
const existingDTs = await fs.readFile(dTsAbsolutePath, 'utf-8').catch(() => null);
|
|
48
|
-
const existingTs = await fs.readFile(tsAbsolutePath, 'utf-8').catch(() => null);
|
|
49
14
|
await fs.mkdir(schemaJsonOutAbsolutePath, { recursive: true });
|
|
50
15
|
await writeConfigJson(schemaOutAbsolutePath, projectInfo);
|
|
51
16
|
// Create JSON files (if not exist) with name [segmentName].json (where segmentName can include /, which means the folder structure can be nested)
|
|
@@ -64,16 +29,6 @@ export default fullSchema;`;
|
|
|
64
29
|
hasChanged = true;
|
|
65
30
|
}
|
|
66
31
|
}));
|
|
67
|
-
// ignore 1st lines at the files
|
|
68
|
-
if (existingJs?.split('\n').slice(1).join('\n') !== jsContent.split('\n').slice(1).join('\n')) {
|
|
69
|
-
await fs.writeFile(jsAbsolutePath, jsContent);
|
|
70
|
-
}
|
|
71
|
-
if (existingDTs?.split('\n').slice(1).join('\n') !== dTsContent.split('\n').slice(1).join('\n')) {
|
|
72
|
-
await fs.writeFile(dTsAbsolutePath, dTsContent);
|
|
73
|
-
}
|
|
74
|
-
if (existingTs?.split('\n').slice(1).join('\n') !== tsContent.split('\n').slice(1).join('\n')) {
|
|
75
|
-
await fs.writeFile(tsAbsolutePath, tsContent);
|
|
76
|
-
}
|
|
77
32
|
// Recursive function to delete unnecessary JSON files and folders
|
|
78
33
|
async function deleteUnnecessaryJsonFiles(dirPath) {
|
|
79
34
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
package/dist/dev/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import keyBy from 'lodash/keyBy.js';
|
|
|
6
6
|
import capitalize from 'lodash/capitalize.js';
|
|
7
7
|
import debounce from 'lodash/debounce.js';
|
|
8
8
|
import once from 'lodash/once.js';
|
|
9
|
-
import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
|
|
9
|
+
import ensureSchemaFiles, { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
|
|
10
10
|
import writeOneSegmentSchemaFile from './writeOneSegmentSchemaFile.mjs';
|
|
11
11
|
import logDiffResult from './logDiffResult.mjs';
|
|
12
12
|
import ensureClient from '../generate/ensureClient.mjs';
|
|
@@ -193,7 +193,6 @@ export class VovkDev {
|
|
|
193
193
|
throw new Error('Already watching');
|
|
194
194
|
const { log } = this.#projectInfo;
|
|
195
195
|
log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
|
|
196
|
-
await ensureClient(this.#projectInfo);
|
|
197
196
|
// automatically watches segments and modules
|
|
198
197
|
this.#watchConfig(callback);
|
|
199
198
|
}
|
|
@@ -226,7 +225,7 @@ export class VovkDev {
|
|
|
226
225
|
}
|
|
227
226
|
}
|
|
228
227
|
else {
|
|
229
|
-
log.debug(`The file does not contain any controller`);
|
|
228
|
+
log.debug(`The file ${filePath} does not contain any controller`);
|
|
230
229
|
}
|
|
231
230
|
};
|
|
232
231
|
#requestSchema = debounceWithArgs(async (segmentName) => {
|
|
@@ -321,7 +320,8 @@ export class VovkDev {
|
|
|
321
320
|
const apiDirAbsolutePath = path.join(cwd, apiDir);
|
|
322
321
|
const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
|
|
323
322
|
this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
|
|
324
|
-
await
|
|
323
|
+
await ensureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
|
|
324
|
+
await ensureClient(this.#projectInfo);
|
|
325
325
|
const MAX_ATTEMPTS = 5;
|
|
326
326
|
const DELAY = 5000;
|
|
327
327
|
// Request schema every segment in 5 seconds in order to update schema on start
|
|
@@ -27,6 +27,13 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
|
|
|
27
27
|
fullSchema: false,
|
|
28
28
|
origin: null,
|
|
29
29
|
},
|
|
30
|
+
fullSchema: {
|
|
31
|
+
templateName: 'fullSchema',
|
|
32
|
+
templatePath: path.resolve(templatesDir, 'fullSchema/*'),
|
|
33
|
+
outDir: clientOutDirAbsolutePath,
|
|
34
|
+
fullSchema: false,
|
|
35
|
+
origin: null,
|
|
36
|
+
},
|
|
30
37
|
};
|
|
31
38
|
const generateFromStrict = generateFrom.map((template) => {
|
|
32
39
|
if (typeof template === 'string') {
|
|
@@ -49,6 +56,9 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
|
|
|
49
56
|
origin: template.origin ?? null,
|
|
50
57
|
};
|
|
51
58
|
});
|
|
59
|
+
if (['ts', 'main', 'module'].some((template) => generateFromStrict.some((item) => item.templateName === template))) {
|
|
60
|
+
generateFromStrict.unshift(builtIn.fullSchema);
|
|
61
|
+
}
|
|
52
62
|
const templateFiles = [];
|
|
53
63
|
for (const generateFromItem of generateFromStrict) {
|
|
54
64
|
const files = await glob(generateFromItem.templatePath);
|
package/dist/generate/index.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import prettify from '../utils/prettify.mjs';
|
|
|
8
8
|
import getClientTemplates, { DEFAULT_FULL_SCHEMA_FILE_NAME } from './getClientTemplates.mjs';
|
|
9
9
|
import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
|
|
10
10
|
import _ from 'lodash';
|
|
11
|
+
import { ROOT_SEGMENT_SCHEMA_NAME, SEGMENTS_SCHEMA_DIR_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
|
|
11
12
|
export default async function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }) {
|
|
12
13
|
const segmentsSchema = fullSchema.segments;
|
|
13
14
|
const generateFrom = templates ?? projectInfo.config.generateFrom;
|
|
@@ -25,6 +26,7 @@ export default async function generate({ projectInfo, segments, forceNothingWrit
|
|
|
25
26
|
continue;
|
|
26
27
|
}
|
|
27
28
|
const now = Date.now();
|
|
29
|
+
const schemaOutDir = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/'); // windows fix
|
|
28
30
|
// Process each template in parallel
|
|
29
31
|
const processedTemplates = noClient
|
|
30
32
|
? []
|
|
@@ -38,9 +40,12 @@ export default async function generate({ projectInfo, segments, forceNothingWrit
|
|
|
38
40
|
// Data for the EJS templates:
|
|
39
41
|
const t = {
|
|
40
42
|
_, // lodash
|
|
43
|
+
ROOT_SEGMENT_SCHEMA_NAME,
|
|
44
|
+
SEGMENTS_SCHEMA_DIR_NAME,
|
|
41
45
|
apiRoot: origin ? `${origin}/${config.rootEntry}` : apiRoot,
|
|
42
46
|
imports: clientImports,
|
|
43
47
|
fullSchema,
|
|
48
|
+
schemaOutDir,
|
|
44
49
|
segmentMeta: Object.fromEntries(segments.map(({ segmentName, ...s }) => [segmentName, s])),
|
|
45
50
|
};
|
|
46
51
|
if (data.imports instanceof Array) {
|
|
@@ -12,12 +12,10 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, configPa
|
|
|
12
12
|
srcRoot: string;
|
|
13
13
|
config: import("vovk").VovkStrictConfig;
|
|
14
14
|
clientImports: {
|
|
15
|
-
fullSchema: string;
|
|
16
15
|
fetcher: string;
|
|
17
16
|
createRPC: string;
|
|
18
17
|
validateOnClient: string | null;
|
|
19
18
|
module: {
|
|
20
|
-
fullSchema: string;
|
|
21
19
|
fetcher: string;
|
|
22
20
|
createRPC: string;
|
|
23
21
|
validateOnClient: string | null;
|
|
@@ -12,8 +12,6 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, co
|
|
|
12
12
|
});
|
|
13
13
|
const apiRoot = `${config.origin ?? ''}/${config.rootEntry}`;
|
|
14
14
|
const apiDir = path.join(srcRoot, 'app', config.rootEntry);
|
|
15
|
-
const schemaOutImportPath = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/') + // windows fix
|
|
16
|
-
'/main.cjs';
|
|
17
15
|
const log = getLogger(config.logLevel);
|
|
18
16
|
if (configAbsolutePaths.length > 1) {
|
|
19
17
|
log.warn(`Multiple config files found. Using the first one: ${configAbsolutePaths[0]}`);
|
|
@@ -23,12 +21,10 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, co
|
|
|
23
21
|
}
|
|
24
22
|
const getImportPath = (p) => (p.startsWith('.') ? path.relative(config.clientOutDir, p) : p);
|
|
25
23
|
const clientImports = {
|
|
26
|
-
fullSchema: schemaOutImportPath,
|
|
27
24
|
fetcher: getImportPath(config.fetcherImport[0]),
|
|
28
25
|
createRPC: getImportPath(config.createRPCImport[0]),
|
|
29
26
|
validateOnClient: config.validateOnClientImport ? getImportPath(config.validateOnClientImport[0]) : null,
|
|
30
27
|
module: {
|
|
31
|
-
fullSchema: schemaOutImportPath,
|
|
32
28
|
fetcher: getImportPath(config.fetcherImport[1] ?? config.fetcherImport[0]),
|
|
33
29
|
createRPC: getImportPath(config.createRPCImport[1] ?? config.createRPCImport[0]),
|
|
34
30
|
validateOnClient: config.validateOnClientImport
|
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
4
|
import 'dotenv/config';
|
|
6
5
|
import { Command } from 'commander';
|
|
7
6
|
import concurrently from 'concurrently';
|
|
@@ -12,6 +11,7 @@ import locateSegments from './locateSegments.mjs';
|
|
|
12
11
|
import { VovkDev } from './dev/index.mjs';
|
|
13
12
|
import newComponents from './new/index.mjs';
|
|
14
13
|
import initProgram from './initProgram.mjs';
|
|
14
|
+
import { getFullSchemaFromJSON } from './utils/getFullSchemaFromJSON.mjs';
|
|
15
15
|
const program = new Command();
|
|
16
16
|
const packageJSON = JSON.parse(readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf-8'));
|
|
17
17
|
program.name('vovk').description('Vovk CLI').version(packageJSON.version);
|
|
@@ -75,7 +75,7 @@ program
|
|
|
75
75
|
.description('Generate RPC client from schema')
|
|
76
76
|
.option('--out, --client-out-dir <path>', 'path to output directory')
|
|
77
77
|
.option('--template, --templates <templates...>', 'client code templates ("ts", "compiled", "python", "none", a custom path)')
|
|
78
|
-
.option('--emit-full-schema, --full-schema [fileName]', 'generate client with full schema')
|
|
78
|
+
.option('--emit-full-schema, --full-schema-json [fileName]', 'generate client with full schema')
|
|
79
79
|
.option('--prettify', 'prettify output files')
|
|
80
80
|
.option('--config <config>', 'path to config file')
|
|
81
81
|
.action(async (options) => {
|
|
@@ -84,8 +84,7 @@ program
|
|
|
84
84
|
const { cwd, config, apiDir } = projectInfo;
|
|
85
85
|
const segments = await locateSegments({ dir: apiDir, config });
|
|
86
86
|
const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
|
|
87
|
-
const
|
|
88
|
-
const { default: fullSchema } = (await import(schemaImportUrl));
|
|
87
|
+
const fullSchema = await getFullSchemaFromJSON(schemaOutAbsolutePath, projectInfo);
|
|
89
88
|
await generate({
|
|
90
89
|
projectInfo,
|
|
91
90
|
segments,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { glob, readFile, access } from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export async function getFullSchemaFromJSON(schemaOutAbsolutePath, projectInfo) {
|
|
4
|
+
const result = {
|
|
5
|
+
config: {},
|
|
6
|
+
segments: {},
|
|
7
|
+
};
|
|
8
|
+
const { log } = projectInfo;
|
|
9
|
+
// Handle config.json
|
|
10
|
+
const configPath = path.join(schemaOutAbsolutePath, 'config.json');
|
|
11
|
+
try {
|
|
12
|
+
const configContent = await readFile(configPath, 'utf-8');
|
|
13
|
+
result.config = JSON.parse(configContent);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
log.warn(`Warning: config.json not found at ${configPath}. Using empty config as fallback.`);
|
|
17
|
+
result.config = {};
|
|
18
|
+
}
|
|
19
|
+
// Handle segments directory
|
|
20
|
+
const segmentsDir = path.join(schemaOutAbsolutePath, 'segments');
|
|
21
|
+
try {
|
|
22
|
+
await access(segmentsDir); // Check if directory exists
|
|
23
|
+
// Use glob to get all JSON files recursively
|
|
24
|
+
const files = await glob(`${segmentsDir}/**/*.json`);
|
|
25
|
+
// Process each JSON file
|
|
26
|
+
for await (const filePath of files) {
|
|
27
|
+
try {
|
|
28
|
+
const content = await readFile(filePath, 'utf-8');
|
|
29
|
+
const jsonData = JSON.parse(content);
|
|
30
|
+
// Get relative path from segments directory and convert to key
|
|
31
|
+
let relativePath = path
|
|
32
|
+
.relative(segmentsDir, filePath)
|
|
33
|
+
.replace(/\.json$/, '') // Remove .json extension
|
|
34
|
+
.replace(/\\/g, '/'); // Normalize to forward slashes
|
|
35
|
+
// Special case for _root.json
|
|
36
|
+
if (path.basename(filePath) === '_root.json' && path.dirname(filePath) === segmentsDir) {
|
|
37
|
+
relativePath = '';
|
|
38
|
+
}
|
|
39
|
+
result.segments[relativePath] = jsonData;
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
log.warn(`Warning: Failed to process file ${filePath}: ${error}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
log.warn(`Warning: Segments directory not found at ${segmentsDir}. Using empty segments as fallback.`);
|
|
48
|
+
result.segments = {};
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
// Example usage:
|
|
53
|
+
/*
|
|
54
|
+
async function main() {
|
|
55
|
+
try {
|
|
56
|
+
const schema = await getSchemaFromJSON('/path/to/directory');
|
|
57
|
+
console.log(schema);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('Error:', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
main();
|
|
63
|
+
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vovk-cli",
|
|
3
|
-
"version": "0.0.1-draft.
|
|
3
|
+
"version": "0.0.1-draft.115",
|
|
4
4
|
"bin": {
|
|
5
5
|
"vovk": "./dist/index.mjs"
|
|
6
6
|
},
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://vovk.dev",
|
|
37
37
|
"peerDependencies": {
|
|
38
|
-
"vovk": "^3.0.0-draft.
|
|
38
|
+
"vovk": "^3.0.0-draft.108"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
41
|
"vovk-python-client": "*"
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import json
|
|
3
|
-
import requests
|
|
4
|
-
from jsonschema import validate
|
|
5
|
-
from jsonschema.exceptions import ValidationError
|
|
6
|
-
from typing import Optional
|
|
7
|
-
|
|
8
|
-
__all__ = [] # We'll populate this dynamically
|
|
9
|
-
|
|
10
|
-
class ServerError(Exception):
|
|
11
|
-
"""Custom exception for server errors that include statusCode and/or message."""
|
|
12
|
-
def __init__(self, status_code: int, message: str):
|
|
13
|
-
super().__init__(f"[{status_code}] {message}")
|
|
14
|
-
self.status_code = status_code
|
|
15
|
-
self.server_message = message
|
|
16
|
-
|
|
17
|
-
def _load_full_schema() -> dict:
|
|
18
|
-
"""
|
|
19
|
-
Loads the 'full-schema.json' file (which must sit in the same folder as this __init__.py).
|
|
20
|
-
Returns it as a Python dictionary.
|
|
21
|
-
"""
|
|
22
|
-
current_dir = os.path.dirname(__file__)
|
|
23
|
-
schema_path = os.path.join(current_dir, "full-schema.json")
|
|
24
|
-
with open(schema_path, "r", encoding="utf-8") as f:
|
|
25
|
-
return json.load(f)
|
|
26
|
-
|
|
27
|
-
class _RPCBase:
|
|
28
|
-
"""
|
|
29
|
-
Base class that provides a validated HTTP request mechanism.
|
|
30
|
-
All dynamic RPC classes will subclass this.
|
|
31
|
-
"""
|
|
32
|
-
def __init__(self, base_url: str):
|
|
33
|
-
self.base_url = base_url.rstrip("/")
|
|
34
|
-
|
|
35
|
-
def _handle_stream_response(self, resp: requests.Response):
|
|
36
|
-
"""
|
|
37
|
-
Returns a generator that yields JSON objects from a newline-delimited stream.
|
|
38
|
-
It attempts to parse each line as valid JSON.
|
|
39
|
-
If we encounter an 'isError' structure, we raise a ServerError immediately.
|
|
40
|
-
"""
|
|
41
|
-
buffer = ""
|
|
42
|
-
|
|
43
|
-
# We'll use resp.iter_content(...) to handle partial chunks
|
|
44
|
-
# decode_unicode=True gives us str chunks in Python 3.
|
|
45
|
-
for chunk in resp.iter_content(chunk_size=None, decode_unicode=True):
|
|
46
|
-
buffer += chunk
|
|
47
|
-
lines = buffer.split("\n")
|
|
48
|
-
# We'll parse every line except the last, which might still be partial
|
|
49
|
-
for line in lines[:-1]:
|
|
50
|
-
line = line.strip()
|
|
51
|
-
if not line:
|
|
52
|
-
continue # skip empty lines
|
|
53
|
-
|
|
54
|
-
try:
|
|
55
|
-
data = json.loads(line)
|
|
56
|
-
except json.JSONDecodeError:
|
|
57
|
-
# Could happen if line is incomplete, but we got a newline anyway
|
|
58
|
-
continue
|
|
59
|
-
|
|
60
|
-
# If the server signals an error in-stream
|
|
61
|
-
if data.get("isError") and "reason" in data:
|
|
62
|
-
resp.close()
|
|
63
|
-
raise ServerError(resp.status_code, str(data["reason"]))
|
|
64
|
-
|
|
65
|
-
yield data
|
|
66
|
-
|
|
67
|
-
# The last piece (lines[-1]) may be incomplete
|
|
68
|
-
buffer = lines[-1]
|
|
69
|
-
|
|
70
|
-
# If there's leftover data in the buffer (no trailing newline at end):
|
|
71
|
-
leftover = buffer.strip()
|
|
72
|
-
if leftover:
|
|
73
|
-
try:
|
|
74
|
-
data = json.loads(leftover)
|
|
75
|
-
if data.get("isError") and "reason" in data:
|
|
76
|
-
resp.close()
|
|
77
|
-
raise ServerError(resp.status_code, str(data["reason"]))
|
|
78
|
-
yield data
|
|
79
|
-
except json.JSONDecodeError:
|
|
80
|
-
# Not valid JSON or partial leftover
|
|
81
|
-
pass
|
|
82
|
-
|
|
83
|
-
# End of stream; close the connection
|
|
84
|
-
resp.close()
|
|
85
|
-
|
|
86
|
-
def _request(
|
|
87
|
-
self,
|
|
88
|
-
method: str,
|
|
89
|
-
endpoint_path: str,
|
|
90
|
-
query: Optional[dict] = None,
|
|
91
|
-
body: Optional[dict] = None,
|
|
92
|
-
query_schema: Optional[dict] = None,
|
|
93
|
-
body_schema: Optional[dict] = None,
|
|
94
|
-
disable_client_validation: bool = False
|
|
95
|
-
):
|
|
96
|
-
"""
|
|
97
|
-
1. If disable_client_validation is False, validates `query` & `body` (if schemas).
|
|
98
|
-
2. Makes an HTTP request (using `requests` with stream=True).
|
|
99
|
-
3. If the response is not 2xx:
|
|
100
|
-
- parse JSON for a possible error structure
|
|
101
|
-
- or raise requests.HTTPError if not available
|
|
102
|
-
4. If 'x-vovk-stream' == 'true', return a generator that yields JSON objects.
|
|
103
|
-
5. Otherwise, parse and return the actual response (JSON -> dict or fallback to text).
|
|
104
|
-
"""
|
|
105
|
-
# Validate query and body if schemas are provided AND validation not disabled
|
|
106
|
-
if not disable_client_validation:
|
|
107
|
-
if query_schema:
|
|
108
|
-
validate(instance=query or {}, schema=query_schema)
|
|
109
|
-
if body_schema:
|
|
110
|
-
validate(instance=body or {}, schema=body_schema)
|
|
111
|
-
|
|
112
|
-
# Build the final URL
|
|
113
|
-
url = f"{self.base_url}/{endpoint_path}"
|
|
114
|
-
|
|
115
|
-
# Make the request (stream=True to handle streaming)
|
|
116
|
-
resp = requests.request(
|
|
117
|
-
method=method,
|
|
118
|
-
url=url,
|
|
119
|
-
params=query,
|
|
120
|
-
json=body,
|
|
121
|
-
stream=True
|
|
122
|
-
)
|
|
123
|
-
|
|
124
|
-
# Check if status is not 2xx
|
|
125
|
-
if not resp.ok:
|
|
126
|
-
try:
|
|
127
|
-
# Attempt to parse JSON error
|
|
128
|
-
data = resp.json()
|
|
129
|
-
# Example: { "statusCode": 400, "message": "Zod validation failed...", "isError": true }
|
|
130
|
-
if data.get("isError"):
|
|
131
|
-
status_code = data.get("statusCode", resp.status_code)
|
|
132
|
-
message = data.get("message", resp.text)
|
|
133
|
-
resp.close()
|
|
134
|
-
raise ServerError(status_code, message)
|
|
135
|
-
else:
|
|
136
|
-
# Not the structured error we expect - fallback
|
|
137
|
-
resp.raise_for_status()
|
|
138
|
-
except ValueError:
|
|
139
|
-
# If parsing fails, fallback
|
|
140
|
-
resp.raise_for_status()
|
|
141
|
-
|
|
142
|
-
# If we get here, resp is 2xx. Check if streaming is requested.
|
|
143
|
-
if resp.headers.get("x-vovk-stream", "").lower() == "true":
|
|
144
|
-
return self._handle_stream_response(resp)
|
|
145
|
-
|
|
146
|
-
# Non-streaming: parse JSON or return text
|
|
147
|
-
content_type = resp.headers.get("Content-Type", "").lower()
|
|
148
|
-
try:
|
|
149
|
-
if "application/json" in content_type:
|
|
150
|
-
result = resp.json() # parse the body as JSON
|
|
151
|
-
else:
|
|
152
|
-
result = resp.text # fallback if not JSON
|
|
153
|
-
finally:
|
|
154
|
-
# In either case, we can close the connection now since we're reading full body
|
|
155
|
-
resp.close()
|
|
156
|
-
|
|
157
|
-
return result
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
def _build_controller_class(
|
|
161
|
-
controller_name: str,
|
|
162
|
-
controller_spec: dict,
|
|
163
|
-
segment_name: str
|
|
164
|
-
):
|
|
165
|
-
"""
|
|
166
|
-
Builds a dynamic class (subclass of _RPCBase) for a single controller.
|
|
167
|
-
Instead of instance methods, we create class methods so we can call
|
|
168
|
-
them directly on the class (passing base_url, query, body, etc.).
|
|
169
|
-
|
|
170
|
-
The endpoints will be constructed as: `segmentName/prefix/path`.
|
|
171
|
-
If `prefix` or `path` contain placeholder segments like `:id`,
|
|
172
|
-
they can be replaced by passing a `params` dict, e.g. { "id": 123 }
|
|
173
|
-
which would convert "/foo/:id/bar" --> "/foo/123/bar"
|
|
174
|
-
"""
|
|
175
|
-
prefix = controller_spec.get("prefix", "").strip("/")
|
|
176
|
-
handlers = controller_spec.get("handlers", {})
|
|
177
|
-
|
|
178
|
-
class_attrs = {}
|
|
179
|
-
|
|
180
|
-
for handler_name, handler_data in handlers.items():
|
|
181
|
-
# HTTP method (e.g., "GET", "POST", etc.)
|
|
182
|
-
http_method = handler_data["httpMethod"]
|
|
183
|
-
|
|
184
|
-
# Path defined in the schema (may contain ":id", etc.)
|
|
185
|
-
path = handler_data["path"].strip("/")
|
|
186
|
-
|
|
187
|
-
# Combine "segmentName/prefix/path" into a single path
|
|
188
|
-
endpoint_path = f"{segment_name}/{prefix}/{path}".strip("/")
|
|
189
|
-
|
|
190
|
-
# Optional JSON schemas (for query/body)
|
|
191
|
-
validation = handler_data.get("validation", {})
|
|
192
|
-
query_schema = validation.get("query")
|
|
193
|
-
body_schema = validation.get("body")
|
|
194
|
-
|
|
195
|
-
def make_class_method(
|
|
196
|
-
m=http_method,
|
|
197
|
-
ep=endpoint_path,
|
|
198
|
-
q_schema=query_schema,
|
|
199
|
-
b_schema=body_schema,
|
|
200
|
-
name=handler_name
|
|
201
|
-
):
|
|
202
|
-
@classmethod
|
|
203
|
-
def handler(cls, base_url, *, query=None, body=None, params=None, disable_client_validation=False):
|
|
204
|
-
"""
|
|
205
|
-
Class method that instantiates the RPC class (with base_url)
|
|
206
|
-
and immediately calls _request on that instance.
|
|
207
|
-
|
|
208
|
-
:param base_url: The base URL of your API.
|
|
209
|
-
:param query: An optional dict for query parameters.
|
|
210
|
-
:param body: An optional dict for the request JSON body.
|
|
211
|
-
:param params: A dict for path substitutions, e.g. {"id": 42}
|
|
212
|
-
which will replace ":id" in the endpoint path.
|
|
213
|
-
:param disable_client_validation: If True, skip schema validation.
|
|
214
|
-
"""
|
|
215
|
-
final_endpoint_path = ep
|
|
216
|
-
|
|
217
|
-
# Perform path param substitution if needed
|
|
218
|
-
for param_key, param_val in (params or {}).items():
|
|
219
|
-
final_endpoint_path = final_endpoint_path.replace(
|
|
220
|
-
f":{param_key}",
|
|
221
|
-
str(param_val)
|
|
222
|
-
)
|
|
223
|
-
|
|
224
|
-
# Instantiate and make the request
|
|
225
|
-
temp_instance = cls(base_url)
|
|
226
|
-
return temp_instance._request(
|
|
227
|
-
method=m,
|
|
228
|
-
endpoint_path=final_endpoint_path,
|
|
229
|
-
query=query,
|
|
230
|
-
body=body,
|
|
231
|
-
query_schema=q_schema,
|
|
232
|
-
body_schema=b_schema,
|
|
233
|
-
disable_client_validation=disable_client_validation
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
handler.__name__ = name
|
|
237
|
-
return handler
|
|
238
|
-
|
|
239
|
-
# Attach the generated class method for this handler
|
|
240
|
-
class_attrs[handler_name] = make_class_method()
|
|
241
|
-
|
|
242
|
-
# Dynamically create a new subclass of _RPCBase with those methods
|
|
243
|
-
return type(controller_name, (_RPCBase,), class_attrs)
|
|
244
|
-
|
|
245
|
-
def _load_controllers():
|
|
246
|
-
"""
|
|
247
|
-
Reads the entire 'full-schema.json',
|
|
248
|
-
iterates over each top-level segment (like 'xxx', 'yyy'),
|
|
249
|
-
extracts the segmentName + controllers,
|
|
250
|
-
and dynamically builds classes for each controller.
|
|
251
|
-
"""
|
|
252
|
-
data = _load_full_schema()
|
|
253
|
-
all_controllers = {}
|
|
254
|
-
|
|
255
|
-
for segment_key, segment_obj in data.items():
|
|
256
|
-
segment_name = segment_obj.get("segmentName", "").strip("/")
|
|
257
|
-
controllers = segment_obj.get("controllers", {})
|
|
258
|
-
|
|
259
|
-
for ctrl_name, ctrl_spec in controllers.items():
|
|
260
|
-
dynamic_class = _build_controller_class(
|
|
261
|
-
controller_name=ctrl_name,
|
|
262
|
-
controller_spec=ctrl_spec,
|
|
263
|
-
segment_name=segment_name
|
|
264
|
-
)
|
|
265
|
-
all_controllers[ctrl_name] = dynamic_class
|
|
266
|
-
|
|
267
|
-
return all_controllers
|
|
268
|
-
|
|
269
|
-
# Build all controllers at import time
|
|
270
|
-
_controllers_dict = _load_controllers()
|
|
271
|
-
|
|
272
|
-
# Export them at the top level
|
|
273
|
-
for ctrl_name, ctrl_class in _controllers_dict.items():
|
|
274
|
-
globals()[ctrl_name] = ctrl_class
|
|
275
|
-
__all__.append(ctrl_name)
|
|
276
|
-
|
package/dist/postinstall.d.mts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/dist/postinstall.mjs
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import getFileSystemEntryType from './utils/getFileSystemEntryType.mjs';
|
|
4
|
-
async function postinstall() {
|
|
5
|
-
// TODO: The function doesn't consider client templates, how to do that?
|
|
6
|
-
const vovk = path.join(import.meta.dirname, '../../.vovk-client');
|
|
7
|
-
const js = path.join(vovk, 'compiled.js');
|
|
8
|
-
const ts = path.join(vovk, 'compiled.d.ts');
|
|
9
|
-
const index = path.join(vovk, 'index.ts');
|
|
10
|
-
await fs.mkdir(vovk, { recursive: true });
|
|
11
|
-
if (!(await getFileSystemEntryType(js))) {
|
|
12
|
-
await fs.writeFile(js, '/* postinstall */');
|
|
13
|
-
}
|
|
14
|
-
if (!(await getFileSystemEntryType(ts))) {
|
|
15
|
-
await fs.writeFile(ts, '/* postinstall */');
|
|
16
|
-
}
|
|
17
|
-
if (!(await getFileSystemEntryType(index))) {
|
|
18
|
-
await fs.writeFile(index, '/* postinstall */');
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
void postinstall();
|