vovk 0.2.3-beta.11 → 0.2.3-beta.110

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 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) => this.#callMethod(types_1._HttpMethod.GET, req, data.params);
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 with JSON data
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',
@@ -32,7 +32,6 @@ class _StreamResponse extends Response {
32
32
  }
33
33
  async throw(e) {
34
34
  const { writer } = this;
35
- // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
36
35
  await this.send({ isError: true, reason: e instanceof Error ? e.message : e });
37
36
  await new Promise((resolve) => setTimeout(resolve, 0));
38
37
  return writer.abort(e);
package/cli/.eslintrc.js CHANGED
@@ -7,6 +7,14 @@ module.exports = {
7
7
  '@typescript-eslint/no-unsafe-call': 'off',
8
8
  '@typescript-eslint/no-unsafe-member-access': 'off',
9
9
  '@typescript-eslint/no-unsafe-return': 'off',
10
- 'no-console': ['error', { allow: ['info', 'error'] }]
11
- }
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
+ },
12
20
  }
@@ -1,20 +1,52 @@
1
+ // @ts-check
1
2
  const fs = require('fs/promises');
2
3
  const path = require('path');
3
- const getVovkrc = require('./getVovkrc');
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
+
4
16
  /**
5
- * Generates client code with string concatenation so it should be super fast
6
- * @type {(rcPath: string) => Promise<void>}
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 }>}
7
20
  */
8
- async function generateClient(rcPath) {
9
- const vovkrc = getVovkrc(rcPath);
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;
10
35
 
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;
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
+ }
15
41
 
16
- const controllersPath = path.join('../..', vovkrc.route).replace(/\.ts$/, '');
17
- let ts = `import type { Controllers } from "${controllersPath}";
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}";
18
50
  import type { clientizeController } from 'vovk/client';
19
51
  import type { promisifyWorker } from 'vovk/worker';
20
52
  import type { VovkClientFetcher } from 'vovk/client';
@@ -22,33 +54,67 @@ import type fetcher from '${fetcherPath}';
22
54
 
23
55
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
24
56
  `;
25
- let js = `const { clientizeController } = require('vovk/client');
57
+ let js = `// auto-generated
58
+ /* eslint-disable */
59
+ const { clientizeController } = require('vovk/client');
26
60
  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'};
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}')` : '{}'};
32
65
 
33
66
  `;
34
- const metadataJson = await fs
35
- .readFile(path.join(__dirname, '../../.vovk/vovk-metadata.json'), 'utf-8')
36
- .catch(() => null);
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
+
37
83
  const metadata = JSON.parse(metadataJson || '{}');
38
84
 
39
85
  for (const key of Object.keys(metadata)) {
40
86
  if (key !== 'workers') {
41
- ts += `export const ${key}: ReturnType<typeof clientizeController<Controllers["${key}"], Options>>;\n`;
42
- js += `exports.${key} = clientizeController(metadata.${key}, { fetcher, streamFetcher, validateOnClient, defaultOptions: { prefix } });\n`;
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`;
43
90
  }
44
91
  }
45
92
 
46
- /* for(const key of Object.keys(metadata.workers ?? {})) {
47
- code += `export const ${key} = promisifyWorker<${key}>(metadata.workers.${key});\n`;
48
- } */
49
- await fs.mkdir('../../.vovk', { recursive: true });
50
- await fs.writeFile(path.join(__dirname, '../../.vovk/index.d.ts'), ts);
51
- await fs.writeFile(path.join(__dirname, '../../.vovk/index.js'), js);
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 };
52
118
  }
53
119
 
54
120
  module.exports = generateClient;
package/cli/getVars.js ADDED
@@ -0,0 +1,47 @@
1
+ // @ts-check
2
+
3
+ const path = require('path');
4
+
5
+ /** @type {import('../src').VovkEnv} */
6
+ let vars;
7
+ /** @type {(rcPath: string, options?: { VOVK_CLIENT_OUT?: string; PORT?: string; }) => import('../src').VovkEnv} */
8
+ function getVars(configPath, options = {}) {
9
+ if (vars) return vars;
10
+ /** @type {Required<import('../src').VovkConfig>} */
11
+ const vovkConfig = {
12
+ out: './node_modules/.vovk',
13
+ route: './src/app/api/[[...vovk]]/route.ts',
14
+ fetcher: 'vovk/client/defaultFetcher',
15
+ prefix: '/api',
16
+ validateOnClient: '',
17
+ };
18
+
19
+ try {
20
+ // make PORT available to the config file
21
+ process.env.PORT = options.PORT || process.env.PORT || '3000';
22
+ Object.assign(vovkConfig, require(configPath));
23
+ } catch {
24
+ // noop
25
+ }
26
+
27
+ vars = {
28
+ PORT: options.PORT || process.env.PORT || '3000',
29
+ VOVK_CLIENT_OUT:
30
+ process.env.VOVK_CLIENT_OUT ||
31
+ (options.VOVK_CLIENT_OUT?.startsWith('/')
32
+ ? options.VOVK_CLIENT_OUT
33
+ : options.VOVK_CLIENT_OUT
34
+ ? path.join(process.cwd(), options.VOVK_CLIENT_OUT)
35
+ : null) ||
36
+ vovkConfig.out,
37
+ VOVK_PORT: process.env.VOVK_PORT || '3690',
38
+ VOVK_ROUTE: process.env.VOVK_ROUTE || vovkConfig.route,
39
+ VOVK_FETCHER: process.env.VOVK_FETCHER || vovkConfig.fetcher,
40
+ VOVK_PREFIX: process.env.VOVK_PREFIX || vovkConfig.prefix,
41
+ VOVK_VALIDATE_ON_CLIENT: process.env.VOVK_VALIDATE_ON_CLIENT || vovkConfig.validateOnClient,
42
+ };
43
+
44
+ return vars;
45
+ }
46
+
47
+ module.exports = getVars;
package/cli/index.js CHANGED
@@ -1,77 +1,80 @@
1
1
  #!/usr/bin/env node
2
- const yargs = require('yargs/yargs');
3
- const { hideBin } = require('yargs/helpers');
4
- const { concurrently } = require('concurrently');
2
+ // @ts-check
5
3
  const generateClient = require('./generateClient');
6
4
  const path = require('path');
5
+ const parallel = require('./lib/parallel');
6
+ const getAvailablePort = require('./lib/getAvailablePort');
7
+ const getVars = require('./getVars');
8
+ const parseCommandLineArgs = require('./lib/parseCommandLineArgs');
7
9
 
8
- const builder = {
9
- rc: {
10
- type: 'string',
11
- default: path.join(process.cwd(), '.vovkrc.js'),
12
- describe: 'Path to .vovkrc.js',
13
- },
10
+ const { command, flags, nextArgs } = parseCommandLineArgs();
11
+ const {
12
+ config = path.join(process.cwd(), 'vovk.config.js'), // Path to vovk.config.js
13
+ project = process.cwd(), // Path to Next.js project
14
+ clientOut = path.join(process.cwd(), './node_modules/.vovk'), // Path to output directory
15
+ } = flags;
14
16
 
15
- project: {
16
- type: 'string',
17
- default: process.cwd(),
18
- describe: 'Path to Next.js project',
19
- },
17
+ if (command === 'dev') {
18
+ void (async () => {
19
+ let PORT =
20
+ process.env.PORT ||
21
+ (await getAvailablePort(3000, 30).catch(() => {
22
+ throw new Error(' 🐺 Failed to find available Next port');
23
+ }));
20
24
 
21
- output: {
22
- type: 'string',
23
- default: path.join(__dirname, '../.vovk/index.d.ts'),
24
- describe: 'Path to the wildcard route file',
25
- },
26
- };
25
+ const env = getVars(config, { VOVK_CLIENT_OUT: clientOut, PORT });
27
26
 
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;
27
+ let VOVK_PORT = parseInt(env.VOVK_PORT);
38
28
 
39
- const nextArgs = process.argv.join(' ').split(' -- ')[1] ?? '';
29
+ env.VOVK_PORT = await getAvailablePort(VOVK_PORT, 30).catch(() => {
30
+ throw new Error(' 🐺 Failed to find available Vovk port');
31
+ });
40
32
 
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
- );
33
+ await parallel(
34
+ [
35
+ {
36
+ command: `node ${__dirname}/server.js`,
37
+ name: 'Vovk',
38
+ },
39
+ { command: `cd ${project} && npx next dev ${nextArgs}`, name: 'Next' },
40
+ ],
41
+ env
42
+ ).catch((e) => console.error(e));
43
+ console.info(' 🐺 All processes have ended');
44
+ })();
45
+ } else if (command === 'build') {
46
+ void (async () => {
47
+ const env = getVars(config, { VOVK_CLIENT_OUT: clientOut });
50
48
 
51
- void result.then(() => {
52
- console.info(' 🐺 All processes have completed.');
53
- });
54
- }
49
+ let VOVK_PORT = parseInt(env.VOVK_PORT);
55
50
 
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.');
51
+ env.VOVK_PORT = await getAvailablePort(VOVK_PORT, 30).catch(() => {
52
+ throw new Error(' 🐺 Failed to find available port');
70
53
  });
71
- }
54
+ await parallel(
55
+ [
56
+ {
57
+ command: `node ${__dirname}/server.js --once`,
58
+ name: 'Vovk',
59
+ },
60
+ { command: `cd ${project} && npx next build ${nextArgs}`, name: 'Next' },
61
+ ],
62
+ env
63
+ ).catch((e) => console.error(e));
64
+ })();
65
+ } else if (command === 'generate') {
66
+ const env = getVars(config, { VOVK_CLIENT_OUT: clientOut });
72
67
 
73
- if (argv._.includes('generate')) {
74
- void generateClient(argv.rc, argv.output).then(() => {
75
- console.info(' 🐺 Client generated.');
68
+ void generateClient(env).then(({ path }) => {
69
+ console.info(` 🐺 Client generated in ${path}`);
76
70
  });
71
+ } else if (command === 'help') {
72
+ console.info(` 🐺 Vovk CLI
73
+ dev - Start development server
74
+ build - Build Next.js project
75
+ generate - Generate client
76
+ help - Show this help message`);
77
+ } else {
78
+ console.error(' 🐺 ❌ Invalid command');
79
+ process.exit(1);
77
80
  }
@@ -0,0 +1,32 @@
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
+ /** @type {(startPort: number, maxAttempts: number, attempt?: number) => Promise<string>} */
18
+ function getAvailablePort(startPort, maxAttempts, attempt = 1) {
19
+ return new Promise((resolve, reject) => {
20
+ checkPort(startPort, (isAvailable) => {
21
+ if (isAvailable) {
22
+ resolve(startPort.toString()); // Found an available port
23
+ } else if (attempt < maxAttempts) {
24
+ getAvailablePort(startPort + 1, maxAttempts, attempt + 1).then(resolve, reject);
25
+ } else {
26
+ reject(null);
27
+ }
28
+ });
29
+ });
30
+ }
31
+
32
+ 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,41 @@
1
+ // @ts-check
2
+
3
+ /** @type {(str: string) => string} */
4
+ function toCamelCase(str) {
5
+ return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
6
+ }
7
+
8
+ /** @typedef {{ config?: string; project?: string; clientOut?: string }} Flags */
9
+ /** @typedef {'dev' | 'build' | 'generate' | 'help'} Command */
10
+ function parseCommandLineArgs() {
11
+ const args = process.argv.slice(2); // Slice off node and script path
12
+ let command = /** @type {Command} */ 'EMPTY';
13
+ /** @type {Flags} */
14
+ const flags = {};
15
+ /** @type {string[]} */
16
+ const unparsedArgs = [];
17
+
18
+ let isUnparsed = false;
19
+ for (const arg of args) {
20
+ if (arg === '--') {
21
+ isUnparsed = true;
22
+ continue;
23
+ }
24
+
25
+ if (isUnparsed) {
26
+ unparsedArgs.push(arg);
27
+ } else if (arg.startsWith('--')) {
28
+ const [key, value = true] = arg.slice(2).split('=');
29
+ const camelKey = /** @type {keyof Flags} */ (toCamelCase(key));
30
+ flags[camelKey] = /** @type {string} */ (value);
31
+ } else if (!command) {
32
+ command = /** @type {Command} */ (arg);
33
+ }
34
+ }
35
+
36
+ const nextArgs = unparsedArgs.join(' ');
37
+
38
+ return { command, flags, nextArgs };
39
+ }
40
+
41
+ module.exports = parseCommandLineArgs;