vovk 0.2.3-beta.1 → 0.2.3-beta.100
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/Segment.d.ts +1 -0
- package/Segment.js +29 -4
- package/StreamResponse.js +1 -1
- package/cli/.eslintrc.js +20 -0
- package/cli/generateClient.js +120 -0
- package/cli/getVars.js +41 -0
- package/cli/index.js +88 -0
- package/cli/lib/getAvailablePort.js +31 -0
- package/cli/lib/getReturnPath.js +28 -0
- package/cli/lib/isEqual.js +28 -0
- package/cli/lib/parallel.js +50 -0
- package/cli/postinstall.js +24 -0
- package/cli/server.js +119 -0
- package/client/clientizeController.d.ts +19 -1
- package/client/clientizeController.js +11 -14
- package/client/defaultFetcher.d.ts +1 -1
- package/client/defaultFetcher.js +17 -21
- package/client/defaultHandler.d.ts +2 -0
- package/client/defaultHandler.js +21 -0
- package/client/defaultStreamHandler.d.ts +3 -0
- package/client/{defaultStreamFetcher.js → defaultStreamHandler.js} +25 -37
- package/client/types.d.ts +13 -4
- package/createSegment.d.ts +16 -20
- package/createSegment.js +28 -51
- package/env.d.ts +1 -0
- package/env.js +29 -0
- package/index.d.ts +20 -23
- package/index.js +5 -2
- package/package.json +10 -8
- package/tsconfig.tsbuildinfo +1 -1
- package/types.d.ts +41 -1
- package/worker/promisifyWorker.d.ts +1 -1
- package/worker/promisifyWorker.js +26 -8
- package/worker/types.d.ts +8 -2
- package/client/defaultStreamFetcher.d.ts +0 -5
package/Segment.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { NextRequest } from 'next/server';
|
|
|
2
2
|
import { _HttpMethod as HttpMethod, type _RouteHandler as RouteHandler } from './types';
|
|
3
3
|
export declare class _Segment {
|
|
4
4
|
#private;
|
|
5
|
+
private static getHeadersFromOptions;
|
|
5
6
|
_routes: Record<HttpMethod, Map<{
|
|
6
7
|
name?: string;
|
|
7
8
|
_prefix?: string;
|
package/Segment.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var _a;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports._Segment = void 0;
|
|
4
5
|
const types_1 = require("./types");
|
|
5
6
|
const HttpException_1 = require("./HttpException");
|
|
6
7
|
const StreamResponse_1 = require("./StreamResponse");
|
|
7
8
|
class _Segment {
|
|
9
|
+
static getHeadersFromOptions(options) {
|
|
10
|
+
const corsHeaders = {
|
|
11
|
+
'Access-Control-Allow-Origin': '*',
|
|
12
|
+
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, HEAD',
|
|
13
|
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
|
14
|
+
};
|
|
15
|
+
const headers = {
|
|
16
|
+
...(options?.cors ? corsHeaders : {}),
|
|
17
|
+
...(options?.headers ?? {}),
|
|
18
|
+
};
|
|
19
|
+
return headers;
|
|
20
|
+
}
|
|
8
21
|
_routes = {
|
|
9
22
|
GET: new Map(),
|
|
10
23
|
POST: new Map(),
|
|
@@ -14,18 +27,21 @@ class _Segment {
|
|
|
14
27
|
HEAD: new Map(),
|
|
15
28
|
OPTIONS: new Map(),
|
|
16
29
|
};
|
|
17
|
-
GET = (req, data) =>
|
|
30
|
+
GET = (req, data) => {
|
|
31
|
+
return this.#callMethod(types_1._HttpMethod.GET, req, data.params);
|
|
32
|
+
};
|
|
18
33
|
POST = (req, data) => this.#callMethod(types_1._HttpMethod.POST, req, data.params);
|
|
19
34
|
PUT = (req, data) => this.#callMethod(types_1._HttpMethod.PUT, req, data.params);
|
|
20
35
|
PATCH = (req, data) => this.#callMethod(types_1._HttpMethod.PATCH, req, data.params);
|
|
21
36
|
DELETE = (req, data) => this.#callMethod(types_1._HttpMethod.DELETE, req, data.params);
|
|
22
37
|
HEAD = (req, data) => this.#callMethod(types_1._HttpMethod.HEAD, req, data.params);
|
|
23
38
|
OPTIONS = (req, data) => this.#callMethod(types_1._HttpMethod.OPTIONS, req, data.params);
|
|
24
|
-
#respond = (status, body) => {
|
|
39
|
+
#respond = (status, body, options) => {
|
|
25
40
|
return new Response(JSON.stringify(body), {
|
|
26
41
|
status,
|
|
27
42
|
headers: {
|
|
28
43
|
'Content-Type': 'application/json',
|
|
44
|
+
..._a.getHeadersFromOptions(options),
|
|
29
45
|
},
|
|
30
46
|
});
|
|
31
47
|
};
|
|
@@ -39,6 +55,9 @@ class _Segment {
|
|
|
39
55
|
#callMethod = async (httpMethod, req, params) => {
|
|
40
56
|
const controllers = this._routes[httpMethod];
|
|
41
57
|
const methodParams = {};
|
|
58
|
+
if (params[Object.keys(params)[0]]?.[0] === '__ping') {
|
|
59
|
+
return this.#respond(200, { message: 'pong' });
|
|
60
|
+
}
|
|
42
61
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
43
62
|
const handlers = Object.fromEntries([...controllers.entries()]
|
|
44
63
|
.map(([controller, staticMethods]) => {
|
|
@@ -113,7 +132,12 @@ class _Segment {
|
|
|
113
132
|
(Reflect.has(result, Symbol.asyncIterator) &&
|
|
114
133
|
typeof result[Symbol.asyncIterator] === 'function'));
|
|
115
134
|
if (isIterator && !(result instanceof Array)) {
|
|
116
|
-
const streamResponse = new StreamResponse_1._StreamResponse(
|
|
135
|
+
const streamResponse = new StreamResponse_1._StreamResponse({
|
|
136
|
+
headers: {
|
|
137
|
+
...StreamResponse_1._StreamResponse.defaultHeaders,
|
|
138
|
+
..._a.getHeadersFromOptions(staticMethod._options),
|
|
139
|
+
},
|
|
140
|
+
});
|
|
117
141
|
void (async () => {
|
|
118
142
|
try {
|
|
119
143
|
for await (const chunk of result) {
|
|
@@ -130,7 +154,7 @@ class _Segment {
|
|
|
130
154
|
if (result instanceof Response) {
|
|
131
155
|
return result;
|
|
132
156
|
}
|
|
133
|
-
return this.#respond(200, result ?? null);
|
|
157
|
+
return this.#respond(200, result ?? null, staticMethod._options);
|
|
134
158
|
}
|
|
135
159
|
catch (e) {
|
|
136
160
|
const err = e;
|
|
@@ -150,3 +174,4 @@ class _Segment {
|
|
|
150
174
|
};
|
|
151
175
|
}
|
|
152
176
|
exports._Segment = _Segment;
|
|
177
|
+
_a = _Segment;
|
package/StreamResponse.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports._StreamResponse = void 0;
|
|
4
4
|
class _StreamResponse extends Response {
|
|
5
|
-
static JSON_DIVIDER = '__##DIV123##__'; // protects collisions
|
|
5
|
+
static JSON_DIVIDER = '__##DIV123##__'; // protects collisions of JSON data
|
|
6
6
|
static defaultHeaders = {
|
|
7
7
|
'Content-Type': 'text/event-stream',
|
|
8
8
|
Connection: 'keep-alive',
|
package/cli/.eslintrc.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
extends: '../.eslintrc.js',
|
|
3
|
+
rules: {
|
|
4
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
5
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
6
|
+
'@typescript-eslint/no-var-requires': 'off',
|
|
7
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
8
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
9
|
+
'@typescript-eslint/no-unsafe-return': 'off',
|
|
10
|
+
'no-console': ['error', { allow: ['info', 'error', 'warn'] }]
|
|
11
|
+
},
|
|
12
|
+
parserOptions: {
|
|
13
|
+
ecmaVersion: 2022,
|
|
14
|
+
sourceType: 'module',
|
|
15
|
+
project: '../tsconfig.cli.json',
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
17
|
+
tsconfigRootDir: __dirname,
|
|
18
|
+
createDefaultProgram: true,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const getReturnPath = require('./lib/getReturnPath');
|
|
5
|
+
|
|
6
|
+
/** @type {(moduleName: string) => boolean} */
|
|
7
|
+
function canRequire(moduleName) {
|
|
8
|
+
try {
|
|
9
|
+
require.resolve(moduleName);
|
|
10
|
+
return true; // The module exists and can be required
|
|
11
|
+
} catch (e) {
|
|
12
|
+
return false; // The module does not exist
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates client code with string concatenation so it should be much faster than using AST
|
|
18
|
+
* TODO: Check fetcher for existence
|
|
19
|
+
* @type {(rcPath: import('../src').VovkEnv) => Promise<{ written: boolean; path: string }>}
|
|
20
|
+
*/
|
|
21
|
+
async function generateClient({ ...env }) {
|
|
22
|
+
const outDir = env.VOVK_CLIENT_OUT;
|
|
23
|
+
const returnDir = getReturnPath(outDir, process.cwd());
|
|
24
|
+
const jsonPath = path.join(returnDir, '.vovk.json');
|
|
25
|
+
const localJsonPath = path.join(process.cwd(), '.vovk.json');
|
|
26
|
+
const fetcherPath = env.VOVK_FETCHER.startsWith('.') ? path.join(returnDir, env.VOVK_FETCHER) : env.VOVK_FETCHER;
|
|
27
|
+
|
|
28
|
+
if (!env.VOVK_VALIDATE_ON_CLIENT) {
|
|
29
|
+
env.VOVK_VALIDATE_ON_CLIENT = canRequire('vovk-zod/zodValidateOnClient') ? 'vovk-zod/zodValidateOnClient' : '';
|
|
30
|
+
}
|
|
31
|
+
const validatePath = env.VOVK_VALIDATE_ON_CLIENT.startsWith('.')
|
|
32
|
+
? path.join(returnDir, env.VOVK_VALIDATE_ON_CLIENT)
|
|
33
|
+
: env.VOVK_VALIDATE_ON_CLIENT;
|
|
34
|
+
const localValidatePath = env.VOVK_VALIDATE_ON_CLIENT.startsWith('.') ? path.join('..', validatePath) : validatePath;
|
|
35
|
+
|
|
36
|
+
if (env.VOVK_VALIDATE_ON_CLIENT && !canRequire(localValidatePath)) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Unble to generate Vovk Client: cannot find "validateOnClient" module '${env.VOVK_VALIDATE_ON_CLIENT}'. Check your vovk.config.js file`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!canRequire(localJsonPath)) {
|
|
43
|
+
throw new Error(`Unble to generate Vovk Client: cannot find ".vovk.json" file '${jsonPath}'.`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const controllersPath = path.join(returnDir, env.VOVK_ROUTE).replace(/\.ts$/, '');
|
|
47
|
+
let dts = `// auto-generated
|
|
48
|
+
/* eslint-disable */
|
|
49
|
+
import type { Controllers, Workers } from "${controllersPath}";
|
|
50
|
+
import type { clientizeController } from 'vovk/client';
|
|
51
|
+
import type { promisifyWorker } from 'vovk/worker';
|
|
52
|
+
import type { VovkClientFetcher } from 'vovk/client';
|
|
53
|
+
import type fetcher from '${fetcherPath}';
|
|
54
|
+
|
|
55
|
+
type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
56
|
+
`;
|
|
57
|
+
let js = `// auto-generated
|
|
58
|
+
/* eslint-disable */
|
|
59
|
+
const { clientizeController } = require('vovk/client');
|
|
60
|
+
const { promisifyWorker } = require('vovk/worker');
|
|
61
|
+
const metadata = require('${jsonPath}');
|
|
62
|
+
const { default: fetcher } = require('${fetcherPath}');
|
|
63
|
+
const prefix = '${env.VOVK_PREFIX ?? '/api'}';
|
|
64
|
+
const { default: validateOnClient = null } = ${validatePath ? `require('${validatePath}')` : '{}'};
|
|
65
|
+
|
|
66
|
+
`;
|
|
67
|
+
let ts = `// auto-generated
|
|
68
|
+
/* eslint-disable */
|
|
69
|
+
import type { Controllers, Workers } from "${controllersPath}";
|
|
70
|
+
import { clientizeController } from 'vovk/client';
|
|
71
|
+
import { promisifyWorker } from 'vovk/worker';
|
|
72
|
+
import type { VovkClientFetcher } from 'vovk/client';
|
|
73
|
+
import fetcher from '${fetcherPath}';
|
|
74
|
+
import metadata from '${jsonPath}';
|
|
75
|
+
${validatePath ? `import validateOnClient from '${validatePath}';\n` : '\nconst validateOnClient = undefined;'}
|
|
76
|
+
const prefix = '${env.VOVK_PREFIX ?? '/api'}';
|
|
77
|
+
type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
|
|
78
|
+
`;
|
|
79
|
+
const metadataJson = await fs.readFile(localJsonPath, 'utf-8').catch(() => null);
|
|
80
|
+
|
|
81
|
+
if (!metadataJson) console.warn(` 🐺 No .vovk.json file found in ${localJsonPath}`);
|
|
82
|
+
|
|
83
|
+
const metadata = JSON.parse(metadataJson || '{}');
|
|
84
|
+
|
|
85
|
+
for (const key of Object.keys(metadata)) {
|
|
86
|
+
if (key !== 'workers') {
|
|
87
|
+
dts += `export const ${key}: ReturnType<typeof clientizeController<Controllers["${key}"], Options>>;\n`;
|
|
88
|
+
js += `exports.${key} = clientizeController(metadata.${key}, { fetcher, validateOnClient, defaultOptions: { prefix } });\n`;
|
|
89
|
+
ts += `export const ${key} = clientizeController<Controllers["${key}"], Options>(metadata.${key}, { fetcher, validateOnClient, defaultOptions: { prefix } });\n`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
for (const key of Object.keys(metadata.workers ?? {})) {
|
|
94
|
+
dts += `export const ${key}: ReturnType<typeof promisifyWorker<Workers["${key}"]>>;\n`;
|
|
95
|
+
js += `exports.${key} = promisifyWorker(null, metadata.workers.${key});\n`;
|
|
96
|
+
ts += `export const ${key} = promisifyWorker<Workers["${key}"]>(null, metadata.workers.${key});\n`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* js += `
|
|
100
|
+
if(typeof window !== 'undefined') fetch(prefix + '/__ping', { method: 'POST' });
|
|
101
|
+
`; */
|
|
102
|
+
|
|
103
|
+
const localJsPath = path.join(outDir, 'client.js');
|
|
104
|
+
const localDtsPath = path.join(outDir, 'client.d.ts');
|
|
105
|
+
const localTsPath = path.join(outDir, 'index.ts');
|
|
106
|
+
const existingJs = await fs.readFile(localJsPath, 'utf-8').catch(() => '');
|
|
107
|
+
const existingDts = await fs.readFile(localDtsPath, 'utf-8').catch(() => '');
|
|
108
|
+
const existingTs = await fs.readFile(localTsPath, 'utf-8').catch(() => '');
|
|
109
|
+
|
|
110
|
+
if (existingJs === js && existingDts === dts && existingTs === ts) return { written: false, path: outDir };
|
|
111
|
+
|
|
112
|
+
await fs.mkdir(outDir, { recursive: true });
|
|
113
|
+
await fs.writeFile(localJsPath, js);
|
|
114
|
+
await fs.writeFile(localDtsPath, dts);
|
|
115
|
+
await fs.writeFile(localTsPath, ts);
|
|
116
|
+
|
|
117
|
+
return { written: true, path: outDir };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = generateClient;
|
package/cli/getVars.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/** @type {import('../src').VovkEnv} */
|
|
5
|
+
/** @type {(rcPath: string, options?: { VOVK_CLIENT_OUT?: string; }) => import('../src').VovkEnv} */
|
|
6
|
+
function getVars(configPath, options = {}) {
|
|
7
|
+
/** @type {Required<import('../src').VovkConfig>} */
|
|
8
|
+
const vovkConfig = {
|
|
9
|
+
out: './node_modules/.vovk',
|
|
10
|
+
route: './src/app/api/[[...vovk]]/route.ts',
|
|
11
|
+
fetcher: 'vovk/client/defaultFetcher',
|
|
12
|
+
prefix: '/api',
|
|
13
|
+
validateOnClient: '',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
Object.assign(vovkConfig, require(configPath));
|
|
18
|
+
} catch {
|
|
19
|
+
// noop
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const vars = {
|
|
23
|
+
VOVK_CLIENT_OUT:
|
|
24
|
+
process.env.VOVK_CLIENT_OUT ||
|
|
25
|
+
(options.VOVK_CLIENT_OUT?.startsWith('/')
|
|
26
|
+
? options.VOVK_CLIENT_OUT
|
|
27
|
+
: options.VOVK_CLIENT_OUT
|
|
28
|
+
? path.join(process.cwd(), options.VOVK_CLIENT_OUT)
|
|
29
|
+
: null) ||
|
|
30
|
+
vovkConfig.out,
|
|
31
|
+
VOVK_PORT: process.env.VOVK_PORT || '3690',
|
|
32
|
+
VOVK_ROUTE: process.env.VOVK_ROUTE || vovkConfig.route,
|
|
33
|
+
VOVK_FETCHER: process.env.VOVK_FETCHER || vovkConfig.fetcher,
|
|
34
|
+
VOVK_PREFIX: process.env.VOVK_PREFIX || vovkConfig.prefix,
|
|
35
|
+
VOVK_VALIDATE_ON_CLIENT: process.env.VOVK_VALIDATE_ON_CLIENT || vovkConfig.validateOnClient,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return vars;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = getVars;
|
package/cli/index.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// @ts-check
|
|
3
|
+
const yargs = require('yargs/yargs');
|
|
4
|
+
const { hideBin } = require('yargs/helpers');
|
|
5
|
+
const generateClient = require('./generateClient');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const parallel = require('./lib/parallel');
|
|
8
|
+
const getAvailablePort = require('./lib/getAvailablePort');
|
|
9
|
+
const getVars = require('./getVars');
|
|
10
|
+
|
|
11
|
+
const builder = {
|
|
12
|
+
config: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: path.join(process.cwd(), 'vovk.config.js'),
|
|
15
|
+
describe: 'Path to vovk.config.js',
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
project: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
default: process.cwd(),
|
|
21
|
+
describe: 'Path to Next.js project',
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
clientOut: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
default: path.join(process.cwd(), './node_modules/.vovk'),
|
|
27
|
+
describe: 'Path to output directory',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** @type {{ config: string, project: string, clientOut: string }} */
|
|
32
|
+
// @ts-expect-error yargs
|
|
33
|
+
const argv = yargs(hideBin(process.argv)) // @ts-expect-error yargs
|
|
34
|
+
.command('dev', 'Run development server', builder) // @ts-expect-error yargs
|
|
35
|
+
.command('build', 'Build the app', builder) // @ts-expect-error yargs
|
|
36
|
+
.command('generate', 'Generate client', builder).argv;
|
|
37
|
+
|
|
38
|
+
const nextArgs = process.argv.join(' ').split(' -- ')[1] ?? '';
|
|
39
|
+
|
|
40
|
+
const env = getVars(argv.config, { VOVK_CLIENT_OUT: argv.clientOut });
|
|
41
|
+
|
|
42
|
+
let VOVK_PORT = parseInt(env.VOVK_PORT);
|
|
43
|
+
|
|
44
|
+
// @ts-expect-error yargs
|
|
45
|
+
if (argv._.includes('dev')) {
|
|
46
|
+
void (async () => {
|
|
47
|
+
env.VOVK_PORT = await getAvailablePort(VOVK_PORT, 20).catch(() => {
|
|
48
|
+
throw new Error(' 🐺 Failed to find available port');
|
|
49
|
+
});
|
|
50
|
+
await parallel(
|
|
51
|
+
[
|
|
52
|
+
{
|
|
53
|
+
command: `node ${__dirname}/server.js`,
|
|
54
|
+
name: 'Vovk',
|
|
55
|
+
},
|
|
56
|
+
{ command: `cd ${argv.project} && npx next dev ${nextArgs}`, name: 'Next' },
|
|
57
|
+
],
|
|
58
|
+
env
|
|
59
|
+
).catch((e) => console.error(e));
|
|
60
|
+
console.info(' 🐺 All processes have completed');
|
|
61
|
+
})();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// @ts-expect-error yargs
|
|
65
|
+
if (argv._.includes('build')) {
|
|
66
|
+
void (async () => {
|
|
67
|
+
env.VOVK_PORT = await getAvailablePort(VOVK_PORT, 20).catch(() => {
|
|
68
|
+
throw new Error(' 🐺 Failed to find available port');
|
|
69
|
+
});
|
|
70
|
+
await parallel(
|
|
71
|
+
[
|
|
72
|
+
{
|
|
73
|
+
command: `node ${__dirname}/server.js --once`,
|
|
74
|
+
name: 'Vovk',
|
|
75
|
+
},
|
|
76
|
+
{ command: `cd ${argv.project} && npx next build ${nextArgs}`, name: 'Next' },
|
|
77
|
+
],
|
|
78
|
+
env
|
|
79
|
+
).catch((e) => console.error(e));
|
|
80
|
+
})();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// @ts-expect-error yargs
|
|
84
|
+
if (argv._.includes('generate')) {
|
|
85
|
+
void generateClient(env).then(({ path }) => {
|
|
86
|
+
console.info(` 🐺 Client generated in ${path}`);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const net = require('net');
|
|
2
|
+
|
|
3
|
+
function checkPort(port, callback) {
|
|
4
|
+
const server = net.createServer();
|
|
5
|
+
|
|
6
|
+
server.listen(port, () => {
|
|
7
|
+
server.close(() => {
|
|
8
|
+
callback(true); // Port is available
|
|
9
|
+
});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
server.on('error', () => {
|
|
13
|
+
callback(false);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getAvailablePort(startPort, maxAttempts, attempt = 1) {
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
checkPort(startPort, (isAvailable) => {
|
|
20
|
+
if (isAvailable) {
|
|
21
|
+
resolve(startPort); // Found an available port
|
|
22
|
+
} else if (attempt < maxAttempts) {
|
|
23
|
+
getAvailablePort(startPort + 1, maxAttempts, attempt + 1).then(resolve, reject);
|
|
24
|
+
} else {
|
|
25
|
+
reject(null);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = getAvailablePort;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @type {(fromPath: string, toPath: string) => string} */
|
|
3
|
+
function getReturnPath(fromPath, toPath) {
|
|
4
|
+
// Split the paths into components
|
|
5
|
+
const fromParts = fromPath.replace(/^\.?\/|\/$/g, '').split('/');
|
|
6
|
+
const toParts = toPath.replace(/^\.?\/|\/$/g, '').split('/');
|
|
7
|
+
|
|
8
|
+
// Find the common base path length
|
|
9
|
+
const length = Math.min(fromParts.length, toParts.length);
|
|
10
|
+
let commonBaseLength = 0;
|
|
11
|
+
for (let i = 0; i < length; i++) {
|
|
12
|
+
if (fromParts[i] !== toParts[i]) break;
|
|
13
|
+
commonBaseLength++;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Calculate steps up to the common base
|
|
17
|
+
const stepsUp = '../'.repeat(fromParts.length - commonBaseLength);
|
|
18
|
+
|
|
19
|
+
// Calculate steps down to the target path
|
|
20
|
+
const stepsDown = toParts.slice(commonBaseLength).join('/');
|
|
21
|
+
|
|
22
|
+
// Combine steps up and steps down
|
|
23
|
+
const result = stepsUp + stepsDown;
|
|
24
|
+
|
|
25
|
+
return result;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = getReturnPath;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @type {(obj1: any, obj2: any) => boolean} */
|
|
3
|
+
const isEqual = (obj1, obj2) => {
|
|
4
|
+
if (obj1 === obj2) {
|
|
5
|
+
return true;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const keys1 = Object.keys(obj1);
|
|
13
|
+
const keys2 = Object.keys(obj2);
|
|
14
|
+
|
|
15
|
+
if (keys1.length !== keys2.length) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
for (const key of keys1) {
|
|
20
|
+
if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
module.exports = isEqual;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const { spawn } = require('child_process');
|
|
3
|
+
|
|
4
|
+
/** @type {(commands: { command: string; name: string; }[], env: import('../../src').VovkEnv) => Promise<void>} */
|
|
5
|
+
function parallel(commands, env) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
/** @type {{ name: string; process: import('child_process').ChildProcess; }[]} */
|
|
8
|
+
let processes = [];
|
|
9
|
+
|
|
10
|
+
commands.forEach((cmd) => {
|
|
11
|
+
const processObj = {
|
|
12
|
+
name: cmd.name,
|
|
13
|
+
process: runCommand(
|
|
14
|
+
cmd.command,
|
|
15
|
+
cmd.name,
|
|
16
|
+
/** @type {(code: number) => void} */
|
|
17
|
+
(code) => handleProcessExit(code, cmd.name)
|
|
18
|
+
),
|
|
19
|
+
};
|
|
20
|
+
processes.push(processObj);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
/** @type {(command: string, name: string, onExit: (code: number) => void) => import('child_process').ChildProcess} */
|
|
24
|
+
function runCommand(command, name, onExit) {
|
|
25
|
+
const proc = spawn(command, { shell: true, env: { ...env, ...process.env }, stdio: 'inherit' });
|
|
26
|
+
|
|
27
|
+
proc.on('exit', onExit);
|
|
28
|
+
|
|
29
|
+
return proc;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @type {(code: number, name: string) => void} */
|
|
33
|
+
function handleProcessExit(code, name) {
|
|
34
|
+
processes = processes.filter((p) => p.name !== name);
|
|
35
|
+
|
|
36
|
+
if (code !== 0) {
|
|
37
|
+
processes.forEach((p) => p.name !== name && p.process.kill('SIGINT'));
|
|
38
|
+
processes = [];
|
|
39
|
+
process.stdout.write('\n');
|
|
40
|
+
return reject(new Error(`Process ${name} exited with code ${code}`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!processes.length) {
|
|
44
|
+
resolve();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = parallel;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const fs = require('fs/promises');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
/** @type {(path: string) => Promise<boolean>} */
|
|
6
|
+
const fileExists = async (path) => !!(await fs.stat(path).catch(() => false));
|
|
7
|
+
|
|
8
|
+
async function postinstall() {
|
|
9
|
+
const vovk = path.join(__dirname, '../../.vovk');
|
|
10
|
+
const js = path.join(vovk, 'client.js');
|
|
11
|
+
const ts = path.join(vovk, 'client.d.ts');
|
|
12
|
+
const index = path.join(vovk, 'index.ts');
|
|
13
|
+
|
|
14
|
+
if ((await fileExists(js)) || (await fileExists(ts)) || (await fileExists(index))) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
await fs.mkdir(vovk, { recursive: true });
|
|
19
|
+
await fs.writeFile(js, '/* postinstall */');
|
|
20
|
+
await fs.writeFile(ts, '/* postinstall */');
|
|
21
|
+
await fs.writeFile(index, '/* postinstall */');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
void postinstall();
|
package/cli/server.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
const http = require('http');
|
|
3
|
+
const fs = require('fs/promises');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const yargs = require('yargs/yargs');
|
|
6
|
+
const { hideBin } = require('yargs/helpers');
|
|
7
|
+
const generateClient = require('./generateClient');
|
|
8
|
+
const getVars = require('./getVars');
|
|
9
|
+
const isEqual = require('./lib/isEqual');
|
|
10
|
+
|
|
11
|
+
/** @type {{ once?: boolean; config: string }} */
|
|
12
|
+
// @ts-expect-error yargs
|
|
13
|
+
const argv = yargs(hideBin(process.argv)).argv;
|
|
14
|
+
|
|
15
|
+
const once = argv.once ?? false;
|
|
16
|
+
|
|
17
|
+
const metadataPath = path.join(__dirname, '../../../.vovk.json');
|
|
18
|
+
|
|
19
|
+
/** @type {(metadata: object) => Promise<{ written: boolean; path: string }>} */
|
|
20
|
+
const writeMetadata = async (metadata) => {
|
|
21
|
+
await fs.mkdir(path.dirname(metadataPath), { recursive: true });
|
|
22
|
+
const existingMetadata = await fs.readFile(metadataPath, 'utf-8').catch(() => 'null');
|
|
23
|
+
if (isEqual(JSON.parse(existingMetadata), metadata)) return { written: false, path: metadataPath };
|
|
24
|
+
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
|
|
25
|
+
return { written: true, path: metadataPath };
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const writeEmptyMetadata = async () => {
|
|
29
|
+
await fs.mkdir(path.dirname(metadataPath), { recursive: true });
|
|
30
|
+
const existingMetadata = await fs.readFile(metadataPath, 'utf-8').catch(() => null);
|
|
31
|
+
if (!existingMetadata) await fs.writeFile(metadataPath, '{}');
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
void writeEmptyMetadata();
|
|
35
|
+
|
|
36
|
+
/** @type {NodeJS.Timeout} */
|
|
37
|
+
let pingInterval;
|
|
38
|
+
|
|
39
|
+
/** @type {import('../src').VovkEnv} */
|
|
40
|
+
let vars;
|
|
41
|
+
|
|
42
|
+
/** @type {(port: string) => void} */
|
|
43
|
+
const startPinging = (PORT) => {
|
|
44
|
+
clearInterval(pingInterval);
|
|
45
|
+
pingInterval = setInterval(() => {
|
|
46
|
+
process.env.PORT = PORT;
|
|
47
|
+
vars = vars ?? getVars(argv.config);
|
|
48
|
+
let prefix = vars.VOVK_PREFIX;
|
|
49
|
+
prefix = prefix.startsWith('http://')
|
|
50
|
+
? prefix
|
|
51
|
+
: `http://localhost:${PORT}/${prefix.startsWith('/') ? prefix.slice(1) : prefix}`;
|
|
52
|
+
const endpoint = `${prefix.endsWith('/') ? prefix.slice(0, -1) : prefix}/__ping`;
|
|
53
|
+
// Create the HTTP GET request
|
|
54
|
+
const req = http.get(endpoint, () => {
|
|
55
|
+
// noop
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Error handling for the request
|
|
59
|
+
req.on('error', (err) => {
|
|
60
|
+
console.error(`🐺 ❌ Error during HTTP request made to ${endpoint}:`, err.message);
|
|
61
|
+
});
|
|
62
|
+
}, 1000 * 3);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const server = http.createServer((req, res) => {
|
|
66
|
+
if (req.method === 'POST' && req.url === '/__metadata') {
|
|
67
|
+
let body = '';
|
|
68
|
+
|
|
69
|
+
req.on('data', (chunk) => {
|
|
70
|
+
body += chunk.toString();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
74
|
+
req.on('end', async () => {
|
|
75
|
+
try {
|
|
76
|
+
/** @type {{ metadata: object; PORT: string }} */
|
|
77
|
+
const { metadata, PORT } = JSON.parse(body); // Parse the JSON data
|
|
78
|
+
const metadataWritten = metadata ? await writeMetadata(metadata) : { written: false, path: metadataPath };
|
|
79
|
+
process.env.PORT = PORT;
|
|
80
|
+
vars = vars ?? getVars(argv.config);
|
|
81
|
+
const codeWritten = await generateClient(vars);
|
|
82
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
83
|
+
res.end('JSON data received and file created');
|
|
84
|
+
if (metadataWritten.written) {
|
|
85
|
+
console.info(` 🐺 JSON metadata updated in ${metadataWritten.path}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (codeWritten.written) {
|
|
89
|
+
console.info(` 🐺 Client generated in ${codeWritten.path}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (PORT && !once) {
|
|
93
|
+
startPinging(PORT);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (once && metadata) server.close();
|
|
97
|
+
} catch (e) {
|
|
98
|
+
const err = /** @type {Error} */ (e);
|
|
99
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
100
|
+
res.end(err?.message ?? 'Error');
|
|
101
|
+
console.error(' 🐺 ❌ ' + err?.message);
|
|
102
|
+
|
|
103
|
+
if (once) server.close();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
108
|
+
res.end('Not Found');
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const VOVK_PORT = process.env.VOVK_PORT;
|
|
113
|
+
if (!VOVK_PORT) {
|
|
114
|
+
console.error(' 🐺 Unable to run Vovk Metadata Server: no port specified');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
server.listen(VOVK_PORT, () => {
|
|
118
|
+
console.info(` 🐺 Vovk Metadata Server is running on port ${VOVK_PORT}`);
|
|
119
|
+
});
|