vovk-cli 0.0.1-beta.0

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.
Files changed (82) hide show
  1. package/.eslintrc.js +20 -0
  2. package/README.md +1 -0
  3. package/dist/getProjectInfo/directoryExists.d.ts +1 -0
  4. package/dist/getProjectInfo/directoryExists.js +16 -0
  5. package/dist/getProjectInfo/getConfig.d.ts +7 -0
  6. package/dist/getProjectInfo/getConfig.js +29 -0
  7. package/dist/getProjectInfo/getCwdPath.d.ts +1 -0
  8. package/dist/getProjectInfo/getCwdPath.js +19 -0
  9. package/dist/getProjectInfo/getSrcRoot.d.ts +1 -0
  10. package/dist/getProjectInfo/getSrcRoot.js +19 -0
  11. package/dist/getProjectInfo/index.d.ts +48 -0
  12. package/dist/getProjectInfo/index.js +78 -0
  13. package/dist/getProjectInfo/readConfig.d.ts +3 -0
  14. package/dist/getProjectInfo/readConfig.js +73 -0
  15. package/dist/index.d.ts +3 -0
  16. package/dist/index.js +104 -0
  17. package/dist/init.d.ts +2 -0
  18. package/dist/init.js +173 -0
  19. package/dist/locateSegments.d.ts +5 -0
  20. package/dist/locateSegments.js +58 -0
  21. package/dist/postinstall.d.ts +1 -0
  22. package/dist/postinstall.js +27 -0
  23. package/dist/server/createMetadataServer.d.ts +5 -0
  24. package/dist/server/createMetadataServer.js +31 -0
  25. package/dist/server/diffMetadata.d.ts +43 -0
  26. package/dist/server/diffMetadata.js +77 -0
  27. package/dist/server/ensureMetadataFiles.d.ts +3 -0
  28. package/dist/server/ensureMetadataFiles.js +100 -0
  29. package/dist/server/generateClient.d.ts +7 -0
  30. package/dist/server/generateClient.js +98 -0
  31. package/dist/server/index.d.ts +6 -0
  32. package/dist/server/index.js +285 -0
  33. package/dist/server/isMetadataEmpty.d.ts +2 -0
  34. package/dist/server/isMetadataEmpty.js +7 -0
  35. package/dist/server/logDiffResult.d.ts +3 -0
  36. package/dist/server/logDiffResult.js +84 -0
  37. package/dist/server/writeOneMetadataFile.d.ts +11 -0
  38. package/dist/server/writeOneMetadataFile.js +34 -0
  39. package/dist/types.d.ts +30 -0
  40. package/dist/types.js +2 -0
  41. package/dist/utils/debounceWithArgs.d.ts +2 -0
  42. package/dist/utils/debounceWithArgs.js +20 -0
  43. package/dist/utils/fileExists.d.ts +1 -0
  44. package/dist/utils/fileExists.js +16 -0
  45. package/dist/utils/getAvailablePort.d.ts +10 -0
  46. package/dist/utils/getAvailablePort.js +47 -0
  47. package/package.json +43 -0
  48. package/src/getProjectInfo/directoryExists.ts +10 -0
  49. package/src/getProjectInfo/getConfig.ts +29 -0
  50. package/src/getProjectInfo/getCwdPath.ts +15 -0
  51. package/src/getProjectInfo/getSrcRoot.ts +14 -0
  52. package/src/getProjectInfo/index.ts +63 -0
  53. package/src/getProjectInfo/readConfig.ts +50 -0
  54. package/src/index.ts +112 -0
  55. package/src/init.ts +174 -0
  56. package/src/locateSegments.ts +40 -0
  57. package/src/postinstall.ts +27 -0
  58. package/src/server/createMetadataServer.ts +30 -0
  59. package/src/server/diffMetadata.ts +110 -0
  60. package/src/server/ensureMetadataFiles.ts +92 -0
  61. package/src/server/generateClient.ts +108 -0
  62. package/src/server/index.ts +306 -0
  63. package/src/server/isMetadataEmpty.ts +6 -0
  64. package/src/server/logDiffResult.ts +114 -0
  65. package/src/server/writeOneMetadataFile.ts +44 -0
  66. package/src/types.ts +58 -0
  67. package/src/utils/debounceWithArgs.ts +22 -0
  68. package/src/utils/fileExists.ts +10 -0
  69. package/src/utils/getAvailablePort.ts +50 -0
  70. package/test/data/segments/[[...vovk]]/route.ts +0 -0
  71. package/test/data/segments/bar/[[...custom]]/route.ts +0 -0
  72. package/test/data/segments/baz/[[...vovk]]/noroute.ts +0 -0
  73. package/test/data/segments/foo/[[...vovk]]/route.ts +0 -0
  74. package/test/data/segments/garply/waldo/route.ts +0 -0
  75. package/test/data/segments/grault/xxxx/[[...vovk]]/noroute.ts +0 -0
  76. package/test/data/segments/quux/corge/[[...vovk]]/route.ts +0 -0
  77. package/test/index.ts +3 -0
  78. package/test/metadata-diff.test.ts +300 -0
  79. package/test/metadata-write.test.ts +82 -0
  80. package/test/utils.test.ts +49 -0
  81. package/tsconfig.json +11 -0
  82. package/tsconfig.test.json +4 -0
@@ -0,0 +1,15 @@
1
+ import path from 'path';
2
+
3
+ // TODO Rename
4
+ export default function getCwdPath<T extends string | null>(inputPath: T, baseDir = process.cwd()): T {
5
+ if (inputPath === null) {
6
+ return null as T;
7
+ }
8
+ // Check if the path is absolute
9
+ if (path.isAbsolute(inputPath) || inputPath.startsWith('./') || inputPath.startsWith('../')) {
10
+ return path.resolve(baseDir, inputPath) as T;
11
+ }
12
+
13
+ // If it's a module or absolute path, keep it as is
14
+ return inputPath;
15
+ }
@@ -0,0 +1,14 @@
1
+ import path from 'path';
2
+ import directoryExists from './directoryExists';
3
+
4
+ export default async function getSrcRoot() {
5
+ const cwd = process.cwd();
6
+ // Next.js Docs: src/app or src/pages will be ignored if app or pages are present in the root directory.
7
+ if (await directoryExists(path.join(cwd, 'app'))) {
8
+ return cwd;
9
+ } else if (await directoryExists(path.join(cwd, 'src/app'))) {
10
+ return path.join(cwd, 'src');
11
+ }
12
+
13
+ throw new Error(`Could not find app router directory. Check Next.js docs for more info.`);
14
+ }
@@ -0,0 +1,63 @@
1
+ import type { VovkEnv } from '../types';
2
+ import path from 'path';
3
+ import * as loglevel from 'loglevel';
4
+ import chalk from 'chalk';
5
+ import getConfig from './getConfig';
6
+
7
+ export type ProjectInfo = Awaited<ReturnType<typeof getProjectInfo>>;
8
+
9
+ // TODO: Rename all occurrences of metadata to schema
10
+ // TODO: Rename default API option "prefix" to "apiRoot" or just "root" (?). Also think of renaming prefix as an option to origin (?)
11
+ // TODO: Load config dynamically to generate client and write schema
12
+ // TODO: Create VovkCLIError class
13
+ export default async function getProjectInfo({
14
+ port: givenPort,
15
+ clientOutDir,
16
+ }: { port?: number; clientOutDir?: string } = {}) {
17
+ const env = process.env as VovkEnv;
18
+ const port = givenPort?.toString() ?? process.env.PORT ?? '3000';
19
+
20
+ // Make PORT available to the config file at getConfig
21
+ process.env.PORT = port;
22
+
23
+ const cwd = process.cwd();
24
+ const { config, srcRoot } = await getConfig({ clientOutDir });
25
+ const vovkPort = env.VOVK_PORT || (parseInt(port) + 6969).toString();
26
+ const apiEntryPoint = `${config.origin}/${config.rootEntry}`; // ??? TODO
27
+ const apiPrefix = `${config.origin}/${config.rootEntry}`; // ??? TODO
28
+ const apiDir = path.join(srcRoot, 'app', config.rootEntry);
29
+
30
+ const metadataOutFullPath = path.join(cwd, config.metadataOutDir);
31
+ const metadataOutImportPath = path.relative(config.clientOutDir, metadataOutFullPath);
32
+ const fetcherClientImportPath = config.fetcher.startsWith('.')
33
+ ? path.relative(config.clientOutDir, config.fetcher)
34
+ : config.fetcher;
35
+
36
+ const clientOutFullPath = path.join(cwd, config.clientOutDir);
37
+
38
+ const log = {
39
+ info: (msg: string) => loglevel.info(chalk.blueBright(`🐺 ${msg}`)),
40
+ warn: (msg: string) => loglevel.warn(chalk.yellowBright(`🐺 ${msg}`)),
41
+ error: (msg: string) => loglevel.error(chalk.redBright(`🐺 ${msg}`)),
42
+ debug: (msg: string) => loglevel.debug(chalk.gray(`🐺 ${msg}`)),
43
+ raw: loglevel,
44
+ };
45
+
46
+ loglevel.setLevel(config.logLevel);
47
+
48
+ return {
49
+ cwd,
50
+ port,
51
+ vovkPort,
52
+ apiEntryPoint,
53
+ apiPrefix,
54
+ apiDir,
55
+ srcRoot,
56
+ metadataOutFullPath,
57
+ metadataOutImportPath,
58
+ clientOutFullPath,
59
+ fetcherClientImportPath,
60
+ config,
61
+ log,
62
+ };
63
+ }
@@ -0,0 +1,50 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+ import type { VovkConfig } from '../types';
4
+
5
+ async function findConfigPath(): Promise<string | null> {
6
+ const rootDir = process.cwd();
7
+ const baseName = 'vovk.config';
8
+ const extensions = ['cjs', 'mjs', 'js'];
9
+
10
+ for (const ext of extensions) {
11
+ const filePath = path.join(rootDir, `${baseName}.${ext}`);
12
+ try {
13
+ await fs.stat(filePath);
14
+ return filePath; // Return the path if the file exists
15
+ } catch {
16
+ // If the file doesn't exist, an error is thrown. Catch it and continue checking.
17
+ }
18
+ }
19
+
20
+ return null; // Return null if no config file was found
21
+ }
22
+
23
+ async function readConfig(): Promise<VovkConfig> {
24
+ const configPath = await findConfigPath();
25
+ let config: VovkConfig = {};
26
+
27
+ if (!configPath) {
28
+ return config;
29
+ }
30
+
31
+ try {
32
+ if (configPath.endsWith('.cjs') || configPath.endsWith('.js')) {
33
+ try {
34
+ delete require.cache[require.resolve(configPath)];
35
+ } finally {
36
+ config = require(configPath) as VovkConfig;
37
+ }
38
+ } else if (configPath.endsWith('.mjs')) {
39
+ const cacheBuster = Date.now();
40
+ ({ default: config } = await import(`${configPath}?cache=${cacheBuster}`));
41
+ }
42
+ } catch (e) {
43
+ // eslint-disable-next-line no-console
44
+ console.error('🐺 ❌ Error reading config file:', (e as Error).message);
45
+ }
46
+
47
+ return config;
48
+ }
49
+
50
+ export default readConfig;
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import concurrently from 'concurrently';
4
+ import getAvailablePort from './utils/getAvailablePort';
5
+ import { VovkCLIServer } from './server';
6
+ import getProjectInfo from './getProjectInfo';
7
+ import generateClient from './server/generateClient';
8
+ import locateSegments from './locateSegments';
9
+ import { VovkConfig, VovkEnv } from './types';
10
+
11
+ /*
12
+ TODO:
13
+ - Use ts-morph to update files
14
+ - Vovk create segment <segmentName>
15
+ - Vovk create module <segmentName>/module.ts
16
+ - Explicit concurrently, implicit concurrently instead of "standalone" mode
17
+ */
18
+
19
+ export type { VovkConfig, VovkEnv };
20
+
21
+ interface DevOptions {
22
+ project: string;
23
+ clientOut?: string;
24
+ nextDev: boolean;
25
+ }
26
+
27
+ interface GenerateOptions {
28
+ clientOut?: string;
29
+ }
30
+
31
+ const program = new Command();
32
+
33
+ program.name('vovk').description('Vovk CLI tool').version('1.0.0');
34
+
35
+ program
36
+ .command('dev')
37
+ .description('Start development server (optional flag --next-dev to start Vovk Server with Next.js)')
38
+ .option('--project <path>', 'Path to Next.js project', process.cwd())
39
+ .option('--client-out <path>', 'Path to client output directory')
40
+ .option('--next-dev', 'Start Vovk Server and Next.js with automatic port allocation', false)
41
+ .allowUnknownOption(true)
42
+ .action(async (options: DevOptions, command: Command) => {
43
+ const portAttempts = 30;
44
+ const PORT = !options.nextDev
45
+ ? process.env.PORT
46
+ : process.env.PORT ||
47
+ (await getAvailablePort(3000, portAttempts, 0, (failedPort, tryingPort) =>
48
+ // eslint-disable-next-line no-console
49
+ console.warn(`🐺 Next.js Port ${failedPort} is in use, trying ${tryingPort} instead.`)
50
+ ).catch(() => {
51
+ throw new Error(`🐺 ❌ Failed to find available Next port after ${portAttempts} attempts`);
52
+ }));
53
+
54
+ if (!PORT) {
55
+ throw new Error('🐺 ❌ PORT env variable is required');
56
+ }
57
+
58
+ if (options.nextDev) {
59
+ const { result } = concurrently(
60
+ [
61
+ {
62
+ command: `node ${__dirname}/server/index.js`,
63
+ name: 'Vovk.ts Metadata Server',
64
+ env: Object.assign(
65
+ { PORT, __VOVK_START_SERVER_IN_STANDALONE_MODE__: 'true' as const },
66
+ options.clientOut ? { VOVK_CLIENT_OUT_DIR: options.clientOut } : {}
67
+ ),
68
+ },
69
+ {
70
+ command: `cd ${options.project} && npx next dev ${command.args.join(' ')}`,
71
+ name: 'Next.js Development Server',
72
+ env: { PORT },
73
+ },
74
+ ],
75
+ {
76
+ killOthers: ['failure', 'success'],
77
+ prefix: 'none',
78
+ }
79
+ );
80
+ try {
81
+ await result;
82
+ } finally {
83
+ // eslint-disable-next-line no-console
84
+ console.log('🐺 Exiting...');
85
+ }
86
+ } else {
87
+ void new VovkCLIServer().startServer({ clientOutDir: options.clientOut });
88
+ }
89
+ });
90
+
91
+ program
92
+ .command('generate')
93
+ .description('Generate client')
94
+ .option('--client-out <path>', 'Path to output directory')
95
+ .action(async (options: GenerateOptions) => {
96
+ const projectInfo = await getProjectInfo({ clientOutDir: options.clientOut });
97
+ const segments = await locateSegments(projectInfo.apiDir);
98
+ const metadata = await import(projectInfo.metadataOutFullPath);
99
+
100
+ await generateClient(projectInfo, segments, metadata.default);
101
+ });
102
+
103
+ program
104
+ .command('help')
105
+ .description('Show help message')
106
+ .action(() => program.help());
107
+
108
+ program.parse(process.argv);
109
+
110
+ if (!process.argv.slice(2).length) {
111
+ program.outputHelp();
112
+ }
package/src/init.ts ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ npx vovk-cli init
4
+ - Check if the project is already initialized
5
+ - Do you want to reinitialize the project?
6
+ - Yes
7
+ - No (exit)
8
+ - Check for package.json, if not found, show error and exit
9
+ - Check for tsconfig.json, if not found, show error and exit
10
+ - Check Next.js installed
11
+ - Choose validation library: add to the installation list
12
+ - vovk-zod
13
+ - Further installation notes: install zod
14
+ - vovk-yup
15
+ - Further installation notes: install yup
16
+ - vovk-dto
17
+ - Further installation notes: install class-validator and class-transformer
18
+ - None
19
+ - If validation library is not None,
20
+ - Do you want to enable client validation?
21
+ - Yes
22
+ - Add client validation to the config
23
+ - No
24
+ - Do you want to use concurrently? (NO NEED, USE CONCURRENTLY BY DEFAULT)
25
+ - Yes (recommended)
26
+ - Add concurrently to the installation list
27
+ - No
28
+ - Do you want to update NPM scripts?
29
+ - Yes
30
+ - Update NPM scripts
31
+ - No
32
+ - if experimentalDecorators is not found in tsconfig.json,
33
+ - Do you want to add experimentalDecorators to tsconfig.json?
34
+ - Yes
35
+ - Add experimentalDecorators to tsconfig.json
36
+ - No
37
+ - Do you want to create route file with example service and controller? (NO NEED)
38
+ - Yes
39
+ - Create route file with example controller
40
+ - No, I will create it myself
41
+ - End
42
+ - If there are any packages to install, install them
43
+ - Show installation notes
44
+ - If there are any files to create, create
45
+ - If there are any config files to update, update
46
+ - If example route file is NOT created, show example route file and controller
47
+ - Show how to run the project
48
+ - If npm scripts are updated
49
+ - npm run dev
50
+ - If npm scripts are NOT updated
51
+ - If concurrently is installed
52
+ - concurrently "vovk dev" "next dev"
53
+ - If concurrently is NOT installed
54
+ - vovk dev --next-dev
55
+ - Open http://localhost:3000/api/hello-world
56
+ - Show how to make a request to the example route
57
+ - Show success message
58
+ */
59
+
60
+ import { confirm } from '@inquirer/prompts';
61
+ // Or
62
+ // import confirm from '@inquirer/confirm';
63
+
64
+ // eslint-disable-next-line no-console
65
+ void confirm({ message: 'Continue?' }).then(console.info);
66
+
67
+ /*
68
+ const wizard = [
69
+ {
70
+ description: 'Check if the project is already initialized',
71
+ type: 'check',
72
+ choices: {
73
+ type: 'choice',
74
+ choices: [
75
+ {
76
+ label: 'Yes',
77
+ action: 'continue',
78
+ },
79
+ {
80
+ label: 'No',
81
+ action: 'exit',
82
+ exitMessage: 'Exiting...',
83
+ },
84
+ ],
85
+ },
86
+ },
87
+ {
88
+ description: 'Check for package.json',
89
+ type: 'check',
90
+ handleCheck: () => {
91
+ // Check if the project is already initialized
92
+ },
93
+ yes: {
94
+ action: 'continue',
95
+ },
96
+ no: {
97
+ action: 'exit',
98
+ exitMessage: 'Exiting...',
99
+ },
100
+ },
101
+ {
102
+ description: 'Check for tsconfig.json',
103
+ type: 'check',
104
+ handleCheck: () => {
105
+ // Check for package.json
106
+ },
107
+ yes: {
108
+ action: 'continue',
109
+ },
110
+ no: {
111
+ action: 'exit',
112
+ exitMessage: 'Exiting...',
113
+ },
114
+ },
115
+ {
116
+ description: 'Check Next.js installed with app router',
117
+ type: 'check',
118
+ handleCheck: () => {
119
+ // Check for tsconfig.json
120
+ },
121
+ yes: {
122
+ action: 'continue',
123
+ },
124
+ no: {
125
+ action: 'exit',
126
+ exitMessage: 'Exiting...',
127
+ },
128
+ },
129
+ {
130
+ description: 'Choose validation library',
131
+ type: 'install',
132
+ choices: {
133
+ type: 'choice',
134
+ choices: [
135
+ {
136
+ package: 'vovk-zod',
137
+ action: 'install',
138
+ notes: 'Further installation notes: install zod',
139
+ },
140
+ {
141
+ package: 'vovk-yup',
142
+ action: 'install',
143
+ notes: 'Further installation notes: install yup',
144
+ },
145
+ {
146
+ package: 'vovk-dto',
147
+ action: 'install',
148
+ notes: 'Further installation notes: install class-validator and class-transformer',
149
+ },
150
+ {
151
+ package: null,
152
+ action: 'continue',
153
+ },
154
+ ],
155
+ },
156
+ },
157
+ {
158
+ description: 'Do you want to enable client validation?',
159
+ type: 'choice',
160
+ choices: [
161
+ {
162
+ label: 'Yes',
163
+ action: 'updateConfig',
164
+ },
165
+ {
166
+ label: 'No',
167
+ action: 'updateConfig',
168
+ },
169
+ ],
170
+ },
171
+ ];
172
+
173
+ // export default console.info(wizard);
174
+ */
@@ -0,0 +1,40 @@
1
+ import { promises as fs } from 'fs';
2
+ import * as path from 'path';
3
+ import fileExists from './utils/fileExists';
4
+
5
+ export type Segment = {
6
+ routeFilePath: string;
7
+ segmentName: string;
8
+ };
9
+
10
+ export default async function locateSegments(dir: string, rootDir = dir): Promise<Segment[]> {
11
+ let results: Segment[] = [];
12
+
13
+ // Read the contents of the directory
14
+ const list = await fs.readdir(dir);
15
+
16
+ // Iterate through each item in the directory
17
+ for (const file of list) {
18
+ const filePath = path.join(dir, file);
19
+ const stat = await fs.stat(filePath);
20
+
21
+ if (stat.isDirectory()) {
22
+ // Check if the directory name matches the pattern [[...something]]
23
+ if (file.startsWith('[[...') && file.endsWith(']]')) {
24
+ // Check if there's a route.ts file inside this directory
25
+ const routeFilePath = path.join(filePath, 'route.ts');
26
+ if (await fileExists(routeFilePath)) {
27
+ // Calculate the basePath relative to the root directory
28
+ const segmentName = path.relative(rootDir, dir);
29
+ results.push({ routeFilePath, segmentName });
30
+ }
31
+ }
32
+
33
+ // Recursively search inside subdirectories
34
+ const subDirResults = await locateSegments(filePath, rootDir);
35
+ results = results.concat(subDirResults);
36
+ }
37
+ }
38
+
39
+ return results;
40
+ }
@@ -0,0 +1,27 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Checks if a file exists at the given path.
6
+ * @param {string} filePath - The path to the file.
7
+ * @returns {Promise<boolean>} - A promise that resolves to true if the file exists, false otherwise.
8
+ */
9
+ const fileExists = async (filePath: string): Promise<boolean> => !!(await fs.stat(filePath).catch(() => false));
10
+
11
+ async function postinstall(): Promise<void> {
12
+ const vovk = path.join(__dirname, '../../.vovk');
13
+ const js = path.join(vovk, 'client.js');
14
+ const ts = path.join(vovk, 'client.d.ts');
15
+ const index = path.join(vovk, 'index.ts');
16
+
17
+ if ((await fileExists(js)) || (await fileExists(ts)) || (await fileExists(index))) {
18
+ return;
19
+ }
20
+
21
+ await fs.mkdir(vovk, { recursive: true });
22
+ await fs.writeFile(js, '/* postinstall */');
23
+ await fs.writeFile(ts, '/* postinstall */');
24
+ await fs.writeFile(index, '/* postinstall */');
25
+ }
26
+
27
+ void postinstall();
@@ -0,0 +1,30 @@
1
+ import http from 'http';
2
+ import { VovkMetadata } from 'vovk';
3
+
4
+ export default function createMetadataServer(
5
+ then: (metadata: { metadata: VovkMetadata }) => void | Promise<void>,
6
+ catchFn: (err: Error) => void | Promise<void>
7
+ ) {
8
+ return http.createServer((req, res) => {
9
+ if (req.method === 'POST' && req.url === '/__metadata') {
10
+ let body = '';
11
+
12
+ req.on('data', (chunk) => {
13
+ body += chunk.toString();
14
+ });
15
+
16
+ req.on('end', () => {
17
+ try {
18
+ const result: { metadata: VovkMetadata } = JSON.parse(body);
19
+ void then(result);
20
+ } catch (e) {
21
+ void catchFn(e as Error);
22
+ }
23
+ });
24
+ } else {
25
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
26
+ res.end('Not Found');
27
+ void catchFn(new Error('Not Found'));
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,110 @@
1
+ import { VovkMetadata } from 'vovk';
2
+ import { _VovkControllerMetadata, _VovkWorkerMetadata } from 'vovk/types';
3
+ import { isEqual } from 'lodash';
4
+
5
+ interface HandlersDiff {
6
+ nameOfClass: string;
7
+ added: string[];
8
+ removed: string[];
9
+ changed: string[];
10
+ }
11
+
12
+ interface WorkersOrControllersDiff {
13
+ added: string[];
14
+ removed: string[];
15
+ handlers: HandlersDiff[];
16
+ }
17
+
18
+ export interface DiffResult {
19
+ workers: WorkersOrControllersDiff;
20
+ controllers: WorkersOrControllersDiff;
21
+ }
22
+
23
+ export function diffHandlers<T extends _VovkWorkerMetadata['_handlers'] | _VovkControllerMetadata['_handlers']>(
24
+ oldHandlers: T,
25
+ newHandlers: T,
26
+ nameOfClass: string
27
+ ): HandlersDiff {
28
+ const added: string[] = [];
29
+ const removed: string[] = [];
30
+ const changed: string[] = []; // Array to store changed handlers
31
+
32
+ for (const [handler, newHandler] of Object.entries(newHandlers)) {
33
+ if (!(handler in oldHandlers)) {
34
+ added.push(handler);
35
+ } else if (!isEqual(newHandler, oldHandlers[handler])) {
36
+ changed.push(handler); // Add to changed if handlers are not shallow equal
37
+ }
38
+ }
39
+
40
+ for (const [handler] of Object.entries(oldHandlers)) {
41
+ if (!(handler in newHandlers)) {
42
+ removed.push(handler);
43
+ }
44
+ }
45
+
46
+ return { nameOfClass, added, removed, changed };
47
+ }
48
+
49
+ export function diffWorkersOrControllers<T extends VovkMetadata['controllers'] | VovkMetadata['workers']>(
50
+ oldItems: T,
51
+ newItems: T
52
+ ): WorkersOrControllersDiff {
53
+ const added: string[] = [];
54
+ const removed: string[] = [];
55
+ const handlersDiff: HandlersDiff[] = [];
56
+
57
+ for (const [item, newItem] of Object.entries(newItems)) {
58
+ if (!(item in oldItems)) {
59
+ added.push(item);
60
+ } else {
61
+ const handlers = diffHandlers(oldItems[item]._handlers, newItem._handlers, item);
62
+ if (handlers.added.length || handlers.removed.length || handlers.changed.length) {
63
+ handlersDiff.push(handlers);
64
+ }
65
+ }
66
+ }
67
+
68
+ for (const [item] of Object.entries(oldItems)) {
69
+ if (!(item in newItems)) {
70
+ removed.push(item);
71
+ }
72
+ }
73
+
74
+ return { added, removed, handlers: handlersDiff };
75
+ }
76
+
77
+ /**
78
+ example output:
79
+ {
80
+ workers: {
81
+ added: ["WorkerC"],
82
+ removed: ["WorkerA"],
83
+ handlers: []
84
+ },
85
+ controllers: {
86
+ added: ["ControllerC"],
87
+ removed: ["ControllerB"],
88
+ handlers: [
89
+ {
90
+ nameOfClass: "ControllerA",
91
+ added: ["handlerF"],
92
+ removed: [],
93
+ changed: ["handlerD"]
94
+ }
95
+ ]
96
+ }
97
+ }
98
+ */
99
+ export default function diffMetadata(oldJson: VovkMetadata, newJson: VovkMetadata): DiffResult {
100
+ const workersDiff = diffWorkersOrControllers<VovkMetadata['workers']>(oldJson.workers ?? {}, newJson.workers ?? {});
101
+ const controllersDiff = diffWorkersOrControllers<VovkMetadata['controllers']>(
102
+ oldJson.controllers ?? {},
103
+ newJson.controllers ?? {}
104
+ );
105
+
106
+ return {
107
+ workers: workersDiff,
108
+ controllers: controllersDiff,
109
+ };
110
+ }