vovk 0.2.3-beta.3 → 0.2.3-beta.4

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.
@@ -0,0 +1,12 @@
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'] }]
11
+ }
12
+ }
@@ -0,0 +1,52 @@
1
+ const fs = require('fs/promises');
2
+ const path = require('path');
3
+ const getVovkrc = require('./getVovkrc');
4
+ /**
5
+ * Generates client code with string concatenation so it should be super fast
6
+ * @type {(rcPath: string) => Promise<void>}
7
+ */
8
+ async function generateClient(rcPath) {
9
+ const vovkrc = getVovkrc(rcPath);
10
+
11
+ const fetcherPath = vovkrc.fetcher.startsWith('.') ? path.join(process.cwd(), vovkrc.fetcher) : vovkrc.fetcher;
12
+ const streamFetcherPath = vovkrc.streamFetcher.startsWith('.')
13
+ ? path.join(process.cwd(), vovkrc.streamFetcher)
14
+ : vovkrc.streamFetcher;
15
+
16
+ const controllersPath = path.join('..', vovkrc.route).replace(/\.ts$/, '');
17
+ let ts = `import type { Controllers } from "${controllersPath}";
18
+ import type { clientizeController } from 'vovk/client';
19
+ import type { promisifyWorker } from 'vovk/worker';
20
+ import type { VovkFetcherOptions } from 'vovk/client';
21
+ import type fetcher from '${fetcherPath}';
22
+
23
+ type Options = typeof fetcher extends VovkFetcherOptions<infer U> ? U : never;
24
+ `;
25
+ let js = `const { clientizeController } = require('vovk/client');
26
+ const { promisifyWorker } = require('vovk/worker');
27
+ const metadata = require('./vovk-metadata.json');
28
+ const fetcher = require('${fetcherPath}');
29
+ const streamFetcher = require('${streamFetcherPath}');
30
+ const prefix = '${vovkrc.prefix ?? '/api'}';
31
+ const validateOnClient = ${vovkrc.validateOnClient ? `require('${vovkrc.validateOnClient}')` : 'null'};
32
+
33
+ `;
34
+ const metadataJson = await fs.readFile(path.join(__dirname, 'vovk-metadata.json'), 'utf-8');
35
+ const metadata = JSON.parse(metadataJson || '{}');
36
+
37
+ for (const key of Object.keys(metadata)) {
38
+ if (key !== 'workers') {
39
+ ts += `export type ${key} = ReturnType<typeof clientizeController<Controllers["${key}"], Options>>;\n`;
40
+ js += `exports.${key} = clientizeController(metadata.${key}, { fetcher, streamFetcher, validateOnClient, defaultOptions: { prefix } });\n`;
41
+ }
42
+ }
43
+
44
+ /* for(const key of Object.keys(metadata.workers ?? {})) {
45
+ code += `export const ${key} = promisifyWorker<${key}>(metadata.workers.${key});\n`;
46
+ } */
47
+
48
+ await fs.writeFile(path.join(__dirname, '../../.vovk/index.d.ts'), ts);
49
+ await fs.writeFile(path.join(__dirname, '../../.vovk/index.js'), js);
50
+ }
51
+
52
+ module.exports = generateClient;
@@ -0,0 +1,28 @@
1
+ const fs = require('fs');
2
+
3
+ let vovkRcRef = null;
4
+
5
+ function getVovkrc(rcPath) {
6
+ if (vovkRcRef) {
7
+ return vovkRcRef;
8
+ }
9
+
10
+ const vovkRc = {
11
+ route: 'src/app/api/[[...]]/route.ts',
12
+ fetcher: 'vovk/client/fetcher',
13
+ streamFetcher: 'vovk/client/streamFetcher',
14
+ prefix: '/api',
15
+ // validateOnClient: 'vovk-zod/validateOnClient',
16
+ };
17
+ if (fs.existsSync(rcPath)) {
18
+ Object.assign(vovkRc, require(rcPath));
19
+ } else {
20
+ console.info(` 🐺 No .vovkrc.js file found in ${process.cwd()}.`);
21
+ }
22
+
23
+ vovkRcRef = vovkRc;
24
+
25
+ return vovkRc;
26
+ }
27
+
28
+ module.exports = getVovkrc;
package/cli/index.js ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ const yargs = require('yargs/yargs');
3
+ const { hideBin } = require('yargs/helpers');
4
+ const { concurrently } = require('concurrently');
5
+ const generateClient = require('./generateClient');
6
+ const path = require('path');
7
+
8
+ const builder = {
9
+ rc: {
10
+ type: 'string',
11
+ default: path.join(process.cwd(), '.vovkrc.js'),
12
+ describe: 'Path to .vovkrc.js',
13
+ },
14
+
15
+ project: {
16
+ type: 'string',
17
+ default: process.cwd(),
18
+ describe: 'Path to Next.js project',
19
+ },
20
+
21
+ output: {
22
+ type: 'string',
23
+ default: path.join(__dirname, '../.vovk/index.d.ts'),
24
+ describe: 'Path to the wildcard route file',
25
+ },
26
+ };
27
+
28
+ const options = {
29
+ outputStream: process.stdout,
30
+ raw: true,
31
+ killOthers: ['failure', 'success'],
32
+ };
33
+ /** @type {{ rc: string, project: string, output: string }} */
34
+ const argv = yargs(hideBin(process.argv))
35
+ .command('dev', 'Run development server', builder)
36
+ .command('build', 'Build the app', builder)
37
+ .command('generate', 'Generate client', builder).argv;
38
+
39
+ const nextArgs = process.argv.join(' ').split(' -- ')[1] ?? '';
40
+
41
+ if (argv._.includes('dev')) {
42
+ const { result } = concurrently(
43
+ [
44
+ { command: `node ${__dirname}/server.js`, name: 'Vovk' },
45
+ { command: `node ${__dirname}/watchMetadata.js --rc ${argv.rc} --output ${argv.output}`, name: 'Watch metadata' },
46
+ { command: `cd ${argv.project} && npx next dev ${nextArgs}`, name: 'Next' },
47
+ ],
48
+ options
49
+ );
50
+
51
+ void result.then(() => {
52
+ console.info(' 🐺 All processes have completed.');
53
+ });
54
+ }
55
+
56
+ if (argv._.includes('build')) {
57
+ const { result } = concurrently(
58
+ [
59
+ { command: `node ${__dirname}/server.js --once`, name: 'Vovk' },
60
+ { command: `cd ${argv.project} && npx next build ${nextArgs}`, name: 'Next' },
61
+ ],
62
+ options
63
+ );
64
+
65
+ void result
66
+ .catch((e) => console.error(e))
67
+ .then(async () => {
68
+ await generateClient(argv.rc, argv.output);
69
+ console.info(' 🐺 Both processes have completed and the client is generated.');
70
+ });
71
+ }
72
+
73
+ if (argv._.includes('generate')) {
74
+ void generateClient(argv.rc, argv.output).then(() => {
75
+ console.info(' 🐺 Client generated.');
76
+ });
77
+ }
package/cli/server.js ADDED
@@ -0,0 +1,76 @@
1
+ const http = require('http');
2
+ const fs = require('fs/promises');
3
+ const path = require('path');
4
+ const yargs = require('yargs/yargs');
5
+ const { hideBin } = require('yargs/helpers');
6
+
7
+ const argv = yargs(hideBin(process.argv)).argv;
8
+
9
+ const once = argv.once ?? false;
10
+
11
+ const isEqual = (obj1, obj2) => {
12
+ if (obj1 === obj2) {
13
+ return true;
14
+ }
15
+
16
+ if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 == null || obj2 == null) {
17
+ return false;
18
+ }
19
+
20
+ const keys1 = Object.keys(obj1);
21
+ const keys2 = Object.keys(obj2);
22
+
23
+ if (keys1.length !== keys2.length) {
24
+ return false;
25
+ }
26
+
27
+ for (const key of keys1) {
28
+ if (!keys2.includes(key) || !isEqual(obj1[key], obj2[key])) {
29
+ return false;
30
+ }
31
+ }
32
+
33
+ return true;
34
+ };
35
+
36
+ const writeMetadata = async (metadataPath, metadata) => {
37
+ await fs.mkdir(path.dirname(metadataPath), { recursive: true });
38
+ const existingMetadata = await fs.readFile(metadataPath, 'utf-8').catch(() => '{}');
39
+ if (isEqual(JSON.parse(existingMetadata), metadata)) return;
40
+ await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2));
41
+ console.info(' 🐺 JSON data received and metadata file created');
42
+ };
43
+
44
+ const server = http.createServer((req, res) => {
45
+ if (req.method === 'POST' && req.url === '/__metadata') {
46
+ let body = '';
47
+
48
+ req.on('data', (chunk) => {
49
+ body += chunk.toString(); // Convert Buffer to string
50
+ });
51
+
52
+ // eslint-disable-next-line @typescript-eslint/no-misused-promises
53
+ req.on('end', async () => {
54
+ try {
55
+ const metadata = JSON.parse(body); // Parse the JSON data
56
+ const filePath = path.join(__dirname, '../.vovk/vovk-metadata.json');
57
+ await writeMetadata(filePath, metadata);
58
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
59
+ res.end('JSON data received and file created');
60
+ } catch (err) {
61
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
62
+ res.end('Invalid JSON');
63
+ console.error(' ❌ ' + err.message);
64
+ }
65
+ if (once) server.close();
66
+ });
67
+ } else {
68
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
69
+ res.end('Not Found');
70
+ }
71
+ });
72
+
73
+ const PORT = process.env.VOVK_PORT || 3420;
74
+ server.listen(PORT, () => {
75
+ console.info(` 🐺 Vovk Server running on port ${PORT}`);
76
+ });
@@ -0,0 +1,178 @@
1
+ {
2
+ "ClientController": {
3
+ "_controllerName": "ClientController",
4
+ "_prefix": "client",
5
+ "_handlers": {
6
+ "getWithParams": {
7
+ "path": "with-params/:hello",
8
+ "httpMethod": "GET"
9
+ },
10
+ "postWithParams": {
11
+ "path": "with-params/:hello",
12
+ "httpMethod": "POST"
13
+ },
14
+ "postWithEqualityValidation": {
15
+ "clientValidators": {
16
+ "body": {
17
+ "hello": "body"
18
+ },
19
+ "query": {
20
+ "hey": "query"
21
+ }
22
+ },
23
+ "path": "client-controller/post-with-equality-validation",
24
+ "httpMethod": "POST"
25
+ },
26
+ "postFormData": {
27
+ "clientValidators": {
28
+ "body": null,
29
+ "query": {
30
+ "type": "object",
31
+ "properties": {
32
+ "hello": {
33
+ "type": "string"
34
+ }
35
+ },
36
+ "required": [
37
+ "hello"
38
+ ],
39
+ "additionalProperties": false,
40
+ "$schema": "http://json-schema.org/draft-07/schema#"
41
+ }
42
+ },
43
+ "path": "client-controller/post-form-data",
44
+ "httpMethod": "POST"
45
+ },
46
+ "postWithZodValidationAndEqualityValidation": {
47
+ "clientValidators": {
48
+ "body": {
49
+ "type": "object",
50
+ "properties": {
51
+ "hello": {
52
+ "type": "string",
53
+ "const": "body"
54
+ }
55
+ },
56
+ "required": [
57
+ "hello"
58
+ ],
59
+ "additionalProperties": false,
60
+ "$schema": "http://json-schema.org/draft-07/schema#"
61
+ },
62
+ "query": {
63
+ "type": "object",
64
+ "properties": {
65
+ "hey": {
66
+ "type": "string",
67
+ "const": "query"
68
+ }
69
+ },
70
+ "required": [
71
+ "hey"
72
+ ],
73
+ "additionalProperties": false,
74
+ "$schema": "http://json-schema.org/draft-07/schema#"
75
+ }
76
+ },
77
+ "path": "client-controller/post-with-zod-validation-and-equality-validation",
78
+ "httpMethod": "POST"
79
+ },
80
+ "getHelloWorld": {
81
+ "path": "client-controller/get-hello-world",
82
+ "httpMethod": "GET"
83
+ },
84
+ "getHelloWorldArray": {
85
+ "path": "client-controller/get-hello-world-array",
86
+ "httpMethod": "GET"
87
+ },
88
+ "getHelloWorldAndEmptyGeneric": {
89
+ "path": "client-controller/get-hello-world-and-empty-generic",
90
+ "httpMethod": "GET"
91
+ }
92
+ }
93
+ },
94
+ "StreamingController": {
95
+ "_controllerName": "StreamingController",
96
+ "_prefix": "streaming",
97
+ "_handlers": {
98
+ "postWithStreaming": {
99
+ "path": "streaming-controller/post-with-streaming",
100
+ "httpMethod": "POST"
101
+ },
102
+ "postWithStreamingAndImmediateError": {
103
+ "path": "streaming-controller/post-with-streaming-and-immediate-error",
104
+ "httpMethod": "POST"
105
+ },
106
+ "postWithStreamingAndDelayedError": {
107
+ "path": "streaming-controller/post-with-streaming-and-delayed-error",
108
+ "httpMethod": "POST"
109
+ },
110
+ "postWithStreamingAndDelayedCustomError": {
111
+ "path": "streaming-controller/post-with-streaming-and-delayed-custom-error",
112
+ "httpMethod": "POST"
113
+ },
114
+ "postWithStreamingAndDelayedUnhandledError": {
115
+ "path": "streaming-controller/post-with-streaming-and-delayed-unhandled-error",
116
+ "httpMethod": "POST"
117
+ }
118
+ }
119
+ },
120
+ "StreamingGeneratorController": {
121
+ "_controllerName": "StreamingGeneratorController",
122
+ "_prefix": "streaming-generator",
123
+ "_handlers": {
124
+ "postWithAsyncStreaming": {
125
+ "path": "streaming-generator-controller/post-with-async-streaming",
126
+ "httpMethod": "POST"
127
+ },
128
+ "postWithStreaming": {
129
+ "path": "streaming-generator-controller/post-with-streaming",
130
+ "httpMethod": "POST"
131
+ },
132
+ "postWithStreamingAndImmediateError": {
133
+ "path": "streaming-generator-controller/post-with-streaming-and-immediate-error",
134
+ "httpMethod": "POST"
135
+ },
136
+ "postWithStreamingAndDelayedError": {
137
+ "path": "streaming-generator-controller/post-with-streaming-and-delayed-error",
138
+ "httpMethod": "POST"
139
+ },
140
+ "postWithStreamingAndDelayedCustomError": {
141
+ "path": "streaming-generator-controller/post-with-streaming-and-delayed-custom-error",
142
+ "httpMethod": "POST"
143
+ },
144
+ "postWithStreamingAndDelayedUnhandledError": {
145
+ "path": "streaming-generator-controller/post-with-streaming-and-delayed-unhandled-error",
146
+ "httpMethod": "POST"
147
+ }
148
+ }
149
+ },
150
+ "workers": {
151
+ "MyWorker": {
152
+ "_workerName": "MyWorker",
153
+ "_handlers": {
154
+ "getHetClientizeHelloWorld": {},
155
+ "calculateFibonacci": {},
156
+ "findLargestPrimeBelow": {},
157
+ "asyncGenerator": {
158
+ "isGenerator": true
159
+ },
160
+ "asyncGeneratorWithError": {
161
+ "isGenerator": true
162
+ },
163
+ "generator": {
164
+ "isGenerator": true
165
+ },
166
+ "generatorWithError": {
167
+ "isGenerator": true
168
+ }
169
+ }
170
+ },
171
+ "MyInnerWorker": {
172
+ "_workerName": "MyInnerWorker",
173
+ "_handlers": {
174
+ "calculateFibonacci": {}
175
+ }
176
+ }
177
+ }
178
+ }
@@ -0,0 +1,20 @@
1
+ const { watch } = require('node:fs/promises');
2
+ const path = require('path');
3
+ const generateClient = require('./generateClient');
4
+
5
+ const yargs = require('yargs/yargs');
6
+ const { hideBin } = require('yargs/helpers');
7
+
8
+ const argv = yargs(hideBin(process.argv)).argv;
9
+
10
+ async function watchMetadata() {
11
+ const jsonWatcher = watch(path.join(__dirname, '../../.vovk/vovk-metadata.json'));
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ for await (const _event of jsonWatcher) {
15
+ await generateClient(argv.rc, argv.output);
16
+ console.info(' 🐺 Client generated');
17
+ }
18
+ }
19
+
20
+ void watchMetadata();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk",
3
- "version": "0.2.3-beta.3",
3
+ "version": "0.2.3-beta.4",
4
4
  "description": "Structural add-on for Next.js",
5
5
  "bin": "./cli/index.js",
6
6
  "scripts": {
@@ -13,7 +13,7 @@
13
13
  "deploy-docs": "npm run --prefix docs deploy",
14
14
  "postpublish": "node ../post-publish-test/post-publish.js",
15
15
  "serve:unit": "npm --prefix test run build && npm --prefix test run start",
16
- "build": "rm -rf dist && npm run toc && tsc && cp package.json dist && cp README.md dist && cp package-lock.json dist",
16
+ "build": "rm -rf dist && npm run toc && tsc && cp package.json dist && cp README.md dist && cp package-lock.json dist && cp -r cli dist",
17
17
  "lint-nofix": "eslint . --ext .ts,.tsx",
18
18
  "lint": "npm run lint-nofix -- --fix",
19
19
  "toc": "npx markdown-toc README.md -i",