vovk-cli 0.0.1-beta.12 → 0.0.1-beta.14

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 (34) hide show
  1. package/dist/{server/generateClient.d.mts → generateClient.d.mts} +2 -2
  2. package/dist/{server/generateClient.mjs → generateClient.mjs} +5 -4
  3. package/dist/getProjectInfo/getConfigPath.d.mts +1 -0
  4. package/dist/getProjectInfo/getConfigPath.mjs +18 -0
  5. package/dist/getProjectInfo/index.d.mts +1 -2
  6. package/dist/getProjectInfo/index.mjs +2 -10
  7. package/dist/getProjectInfo/readConfig.mjs +2 -19
  8. package/dist/index.d.mts +5 -0
  9. package/dist/index.mjs +17 -9
  10. package/dist/init/index.d.mts +20 -0
  11. package/dist/init/index.mjs +244 -0
  12. package/dist/init/installDependencies.d.mts +1 -0
  13. package/dist/init/installDependencies.mjs +28 -0
  14. package/dist/types.d.mts +1 -1
  15. package/dist/utils/formatLoggedSegmentName.d.mts +1 -0
  16. package/dist/utils/formatLoggedSegmentName.mjs +5 -0
  17. package/dist/utils/getLogger.d.mts +8 -0
  18. package/dist/utils/getLogger.mjs +13 -0
  19. package/dist/{server → watcher}/ensureSchemaFiles.mjs +3 -2
  20. package/dist/watcher/index.d.mts +6 -0
  21. package/dist/{server → watcher}/index.mjs +20 -19
  22. package/dist/{server → watcher}/logDiffResult.mjs +10 -9
  23. package/package.json +7 -1
  24. package/dist/init.d.mts +0 -2
  25. package/dist/init.mjs +0 -171
  26. package/dist/server/index.d.mts +0 -6
  27. /package/dist/{server → watcher}/diffSchema.d.mts +0 -0
  28. /package/dist/{server → watcher}/diffSchema.mjs +0 -0
  29. /package/dist/{server → watcher}/ensureSchemaFiles.d.mts +0 -0
  30. /package/dist/{server → watcher}/isMetadataEmpty.d.mts +0 -0
  31. /package/dist/{server → watcher}/isMetadataEmpty.mjs +0 -0
  32. /package/dist/{server → watcher}/logDiffResult.d.mts +0 -0
  33. /package/dist/{server → watcher}/writeOneSchemaFile.d.mts +0 -0
  34. /package/dist/{server → watcher}/writeOneSchemaFile.mjs +0 -0
@@ -1,5 +1,5 @@
1
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- import type { Segment } from '../locateSegments.mjs';
1
+ import type { ProjectInfo } from './getProjectInfo/index.mjs';
2
+ import type { Segment } from './locateSegments.mjs';
3
3
  import type { VovkSchema } from 'vovk';
4
4
  export default function generateClient(projectInfo: ProjectInfo, segments: Segment[], segmentsSchema: Record<string, VovkSchema>): Promise<{
5
5
  written: boolean;
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
+ import formatLoggedSegmentName from './utils/formatLoggedSegmentName.mjs';
3
4
  export default async function generateClient(projectInfo, segments, segmentsSchema) {
4
5
  const now = Date.now();
5
6
  const clientoOutDirFullPath = path.join(projectInfo.cwd, projectInfo.config.clientOutDir);
@@ -34,7 +35,7 @@ import schema from '${projectInfo.schemaOutImportPath}';
34
35
  const { routeFilePath, segmentName } = segments[i];
35
36
  const schema = segmentsSchema[segmentName];
36
37
  if (!schema) {
37
- throw new Error(`Unable to generate client. No schema found for segment ${segmentName}`);
38
+ throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
38
39
  }
39
40
  if (!schema.emitSchema)
40
41
  continue;
@@ -44,12 +45,12 @@ import schema from '${projectInfo.schemaOutImportPath}';
44
45
  }
45
46
  dts += `
46
47
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
47
- `;
48
+ `;
48
49
  ts += `
49
50
  ${validateFullPath ? `import validateOnClient from '${validateFullPath}';\n` : '\nconst validateOnClient = undefined;'}
50
51
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
51
52
  const prefix = '${projectInfo.apiEntryPoint}';
52
- `;
53
+ `;
53
54
  js += `
54
55
  const { default: validateOnClient = null } = ${validateFullPath ? `require('${validateFullPath}')` : '{}'};
55
56
  const prefix = '${projectInfo.apiEntryPoint}';
@@ -58,7 +59,7 @@ const prefix = '${projectInfo.apiEntryPoint}';
58
59
  const { segmentName } = segments[i];
59
60
  const schema = segmentsSchema[segmentName];
60
61
  if (!schema) {
61
- throw new Error(`Unable to generate client. No schema found for segment ${segmentName}`);
62
+ throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
62
63
  }
63
64
  if (!schema.emitSchema)
64
65
  continue;
@@ -0,0 +1 @@
1
+ export default function getConfigPath(relativePath?: string): Promise<string | null>;
@@ -0,0 +1,18 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ export default async function getConfigPath(relativePath = '') {
4
+ const rootDir = path.resolve(process.cwd(), relativePath || '');
5
+ const baseName = 'vovk.config';
6
+ const extensions = ['cjs', 'mjs', 'js'];
7
+ for (const ext of extensions) {
8
+ const filePath = path.join(rootDir, `${baseName}.${ext}`);
9
+ try {
10
+ await fs.stat(filePath);
11
+ return filePath; // Return the path if the file exists
12
+ }
13
+ catch {
14
+ // Empty
15
+ }
16
+ }
17
+ return null; // Return null if no config file was found
18
+ }
@@ -1,4 +1,3 @@
1
- import loglevel from 'loglevel';
2
1
  export type ProjectInfo = Awaited<ReturnType<typeof getProjectInfo>>;
3
2
  export default function getProjectInfo({ port: givenPort, clientOutDir, }?: {
4
3
  port?: number;
@@ -17,6 +16,6 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, }?: {
17
16
  warn: (msg: string) => void;
18
17
  error: (msg: string) => void;
19
18
  debug: (msg: string) => void;
20
- raw: loglevel.RootLogger;
19
+ raw: import("loglevel").RootLogger;
21
20
  };
22
21
  }>;
@@ -1,7 +1,6 @@
1
1
  import path from 'path';
2
- import loglevel from 'loglevel';
3
- import chalk from 'chalk';
4
2
  import getConfig from './getConfig.mjs';
3
+ import getLogger from '../utils/getLogger.mjs';
5
4
  export default async function getProjectInfo({ port: givenPort, clientOutDir, } = {}) {
6
5
  const port = givenPort?.toString() ?? process.env.PORT ?? '3000';
7
6
  // Make PORT available to the config file at getConfig
@@ -14,14 +13,7 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, }
14
13
  const fetcherClientImportPath = config.fetcher.startsWith('.')
15
14
  ? path.relative(config.clientOutDir, config.fetcher)
16
15
  : config.fetcher;
17
- const log = {
18
- info: (msg) => loglevel.info(chalk.blueBright(`🐺 ${msg}`)),
19
- warn: (msg) => loglevel.warn(chalk.yellowBright(`🐺 ${msg}`)),
20
- error: (msg) => loglevel.error(chalk.redBright(`🐺 ${msg}`)),
21
- debug: (msg) => loglevel.debug(chalk.gray(`🐺 ${msg}`)),
22
- raw: loglevel,
23
- };
24
- loglevel.setLevel(config.logLevel);
16
+ const log = getLogger(config.logLevel);
25
17
  return {
26
18
  cwd,
27
19
  port,
@@ -1,23 +1,6 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
3
- async function findConfigPath() {
4
- const rootDir = process.cwd();
5
- const baseName = 'vovk.config';
6
- const extensions = ['cjs', 'mjs', 'js'];
7
- for (const ext of extensions) {
8
- const filePath = path.join(rootDir, `${baseName}.${ext}`);
9
- try {
10
- await fs.stat(filePath);
11
- return filePath; // Return the path if the file exists
12
- }
13
- catch {
14
- // If the file doesn't exist, an error is thrown. Catch it and continue checking.
15
- }
16
- }
17
- return null; // Return null if no config file was found
18
- }
1
+ import getConfigPath from './getConfigPath.mjs';
19
2
  async function readConfig() {
20
- const configPath = await findConfigPath();
3
+ const configPath = await getConfigPath();
21
4
  let config = {};
22
5
  if (!configPath) {
23
6
  return config;
package/dist/index.d.mts CHANGED
@@ -1,3 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { VovkConfig, VovkEnv } from './types.mjs';
3
+ import type { LogLevelNames } from 'loglevel';
3
4
  export type { VovkConfig, VovkEnv };
5
+ export interface InitOptions {
6
+ yes: boolean;
7
+ logLevel: LogLevelNames;
8
+ }
package/dist/index.mjs CHANGED
@@ -2,21 +2,21 @@
2
2
  import { Command } from 'commander';
3
3
  import concurrently from 'concurrently';
4
4
  import getAvailablePort from './utils/getAvailablePort.mjs';
5
- import { VovkCLIServer } from './server/index.mjs';
5
+ import { VovkCLIWatcher } from './watcher/index.mjs';
6
6
  import getProjectInfo from './getProjectInfo/index.mjs';
7
- import generateClient from './server/generateClient.mjs';
7
+ import generateClient from './generateClient.mjs';
8
8
  import locateSegments from './locateSegments.mjs';
9
9
  import path from 'path';
10
10
  import { readFileSync } from 'fs';
11
+ import { Init } from './init/index.mjs';
11
12
  const program = new Command();
12
13
  const packageJSON = JSON.parse(readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf-8'));
13
14
  program.name('vovk').description('Vovk CLI').version(packageJSON.version);
14
15
  program
15
16
  .command('dev')
16
- .description('Start development server (optional flag --next-dev to start Vovk Server with Next.js)')
17
- .option('--project <path>', 'Path to Next.js project', process.cwd())
17
+ .description('Start schema watcher (optional flag --next-dev to start it with Next.js)')
18
18
  .option('--client-out <path>', 'Path to client output directory')
19
- .option('--next-dev', 'Start Vovk Server and Next.js with automatic port allocation', false)
19
+ .option('--next-dev', 'Start schema watcher and Next.js with automatic port allocation', false)
20
20
  .allowUnknownOption(true)
21
21
  .action(async (options, command) => {
22
22
  const portAttempts = 30;
@@ -34,9 +34,9 @@ program
34
34
  if (options.nextDev) {
35
35
  const { result } = concurrently([
36
36
  {
37
- command: `node ${import.meta.dirname}/server/index.mjs`,
38
- name: 'Vovk.ts Schema Server',
39
- env: Object.assign({ PORT, __VOVK_START_SERVER_IN_STANDALONE_MODE__: 'true' }, options.clientOut ? { VOVK_CLIENT_OUT_DIR: options.clientOut } : {}),
37
+ command: `node ${import.meta.dirname}/watcher/index.mjs`,
38
+ name: 'Vovk.ts Schema Watcher',
39
+ env: Object.assign({ PORT, __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true' }, options.clientOut ? { VOVK_CLIENT_OUT_DIR: options.clientOut } : {}),
40
40
  },
41
41
  {
42
42
  command: `cd ${options.project} && npx next dev ${command.args.join(' ')}`,
@@ -56,7 +56,7 @@ program
56
56
  }
57
57
  }
58
58
  else {
59
- void new VovkCLIServer().startServer({ clientOutDir: options.clientOut });
59
+ void new VovkCLIWatcher().start({ clientOutDir: options.clientOut });
60
60
  }
61
61
  });
62
62
  program
@@ -71,6 +71,14 @@ program
71
71
  const schema = (await import(path.join(schemaOutFullPath, 'index.js')));
72
72
  await generateClient(projectInfo, segments, schema.default);
73
73
  });
74
+ program
75
+ .command('init [prefix]')
76
+ .description('Initialize Vovk project')
77
+ .option('-Y, --yes', 'Skip all prompts and use default values')
78
+ .option('--log-level <level>', 'Set log level', 'info')
79
+ .action(async (prefix = '.', options) => {
80
+ await Init.main(prefix, options);
81
+ });
74
82
  program
75
83
  .command('help')
76
84
  .description('Show help message')
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env node
2
+ import type { VovkConfig } from '../types.mjs';
3
+ import type { InitOptions } from '../index.mjs';
4
+ import getLogger from '../utils/getLogger.mjs';
5
+ declare abstract class Action<T> {
6
+ context: Context;
7
+ data: T;
8
+ action: () => void;
9
+ constructor(context: Context);
10
+ toJSON: () => T;
11
+ }
12
+ declare class Context {
13
+ actions: Action<unknown>[];
14
+ vovkConfig: VovkConfig;
15
+ root: string;
16
+ log: ReturnType<typeof getLogger>;
17
+ static main(prefix: string, { yes, logLevel }: InitOptions): Promise<void>;
18
+ }
19
+ export declare const Init: typeof Context;
20
+ export {};
@@ -0,0 +1,244 @@
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 update NPM scripts?
25
+ - Yes
26
+ - Update NPM scripts
27
+ - No
28
+ - Do you want to use explicit concurrently?
29
+ - Yes (recommended)
30
+ - Add concurrently to the installation list
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
+ import { confirm, select } from '@inquirer/prompts';
60
+ import NPMCliPackageJson from '@npmcli/package-json';
61
+ import path from 'path';
62
+ import fs from 'fs/promises';
63
+ import * as jsonc from 'jsonc-parser';
64
+ import getConfigPath from '../getProjectInfo/getConfigPath.mjs';
65
+ import chalk from 'chalk';
66
+ import fileExists from '../utils/fileExists.mjs';
67
+ import installDependencies from './installDependencies.mjs';
68
+ import getLogger from '../utils/getLogger.mjs';
69
+ class Action {
70
+ context;
71
+ data;
72
+ // public prevAction: Action | null = null;
73
+ action;
74
+ constructor(context) {
75
+ this.context = context;
76
+ }
77
+ toJSON = () => this.data;
78
+ }
79
+ class InstallValidationLibraryAction extends Action {
80
+ constructor(context, data) {
81
+ super(context);
82
+ this.data = data;
83
+ }
84
+ action = () => {
85
+ this.context.vovkConfig.validationLibrary = this.data.validationLibrary;
86
+ if (this.data.validationLibrary && this.data.enableClientValidation) {
87
+ this.context.vovkConfig.validateOnClient = `${this.data.validationLibrary}/validateOnClient`;
88
+ }
89
+ };
90
+ }
91
+ class UpdateNpmScriptsAction extends Action {
92
+ constructor(context) {
93
+ super(context);
94
+ this.data = { shouldUpdateNpmScripts: true };
95
+ }
96
+ action = async () => {
97
+ if (this.data.shouldUpdateNpmScripts && typeof this.data.useConcurrently === 'undefined') {
98
+ throw new Error('useConcurrently must be defined');
99
+ }
100
+ const pkgJson = await NPMCliPackageJson.load(this.context.root);
101
+ pkgJson.update({
102
+ scripts: {
103
+ generate: 'vovk generate',
104
+ dev: this.data.useConcurrently
105
+ ? 'PORT=3000 concurrently "vovk dev" "next dev" --kill-others'
106
+ : 'vovk dev --next-dev',
107
+ },
108
+ });
109
+ await pkgJson.save();
110
+ };
111
+ }
112
+ class UpdateTsconfigAction extends Action {
113
+ constructor(context) {
114
+ super(context);
115
+ }
116
+ action = async () => {
117
+ const tsconfigPath = path.resolve(process.cwd(), 'tsconfig.json');
118
+ try {
119
+ // Read the content of tsconfig.json asynchronously
120
+ const tsconfigContent = await fs.readFile(tsconfigPath, 'utf8');
121
+ // Use jsonc-parser to generate edits and modify the experimentalDecorators property
122
+ const edits = jsonc.modify(tsconfigContent, ['compilerOptions', 'experimentalDecorators'], true, {
123
+ formattingOptions: {},
124
+ });
125
+ // Apply the edits to the original content
126
+ const updatedContent = jsonc.applyEdits(tsconfigContent, edits);
127
+ // Write the updated content back to the file asynchronously
128
+ await fs.writeFile(tsconfigPath, updatedContent, 'utf8');
129
+ }
130
+ catch (error) {
131
+ throw new Error(`Failed to update tsconfig.json at ${tsconfigPath}. ${String(error)}`);
132
+ }
133
+ };
134
+ static async checkTsconfigForExperimentalDecorators() {
135
+ const tsconfigPath = path.resolve(process.cwd(), 'tsconfig.json');
136
+ let tsconfigContent;
137
+ try {
138
+ tsconfigContent = await fs.readFile(tsconfigPath, 'utf8');
139
+ }
140
+ catch (error) {
141
+ throw new Error(`Failed to read tsconfig.json at ${tsconfigPath}. You can run "npx tsc --init" to create it. ${String(error)}`);
142
+ }
143
+ const tsconfig = jsonc.parse(tsconfigContent);
144
+ return !!tsconfig?.compilerOptions?.experimentalDecorators;
145
+ }
146
+ }
147
+ class Context {
148
+ actions = [];
149
+ vovkConfig = {};
150
+ root;
151
+ log;
152
+ static async main(prefix, { yes, logLevel }) {
153
+ // TODO handle yes option
154
+ console.log('yes', yes);
155
+ const context = new Context();
156
+ const cwd = process.cwd();
157
+ const configPath = await getConfigPath(prefix);
158
+ const toBeInstalled = ['vovk'];
159
+ const root = path.join(cwd, prefix);
160
+ const log = getLogger(logLevel);
161
+ context.root = root;
162
+ context.log = log;
163
+ if (!(await fileExists(path.join(root, 'package.json')))) {
164
+ throw new Error(`package.json not found at ${root}. Run "npx create-next-app" to create a new Next.js project.`);
165
+ }
166
+ if (!(await fileExists(path.join(root, 'tsconfig.json')))) {
167
+ throw new Error(`tsconfig.json not found at ${root}. Run "npx tsc --init" to create a new tsconfig.json file.`);
168
+ }
169
+ if (configPath) {
170
+ if (!(await confirm({
171
+ message: `Found existing config file at ${configPath}. Do you want to reinitialize the project?`,
172
+ })))
173
+ return;
174
+ }
175
+ const validationLibrary = await select({
176
+ message: 'Choose validation library',
177
+ default: 'vovk-zod',
178
+ choices: [
179
+ {
180
+ name: 'vovk-zod',
181
+ value: 'vovk-zod',
182
+ description: 'Use Zod for data validation',
183
+ },
184
+ {
185
+ name: 'vovk-yup',
186
+ value: 'vovk-yup',
187
+ description: 'Use Yup for data validation',
188
+ },
189
+ {
190
+ name: 'vovk-dto',
191
+ value: 'vovk-dto',
192
+ description: 'Use class-validator and class-transformer for data validation',
193
+ },
194
+ { name: 'None', value: null, description: 'Install validation library later' },
195
+ ],
196
+ });
197
+ if (validationLibrary) {
198
+ toBeInstalled.push(validationLibrary);
199
+ toBeInstalled.push(...({
200
+ 'vovk-zod': ['zod'],
201
+ 'vovk-yup': ['yup'],
202
+ 'vovk-dto': ['class-validator', 'class-transformer'],
203
+ }[validationLibrary] ?? []));
204
+ const installValidationLibraryAction = new InstallValidationLibraryAction(context, { validationLibrary });
205
+ context.actions.push(installValidationLibraryAction);
206
+ installValidationLibraryAction.data.enableClientValidation = await confirm({
207
+ message: 'Do you want to enable client validation?',
208
+ });
209
+ }
210
+ const devScriptType = await select({
211
+ message: 'Do you want to update package.json by adding "generate" and "dev" scripts?',
212
+ default: 'IMPLICIT',
213
+ choices: [
214
+ {
215
+ name: 'Yes, with implicit concurrently',
216
+ description: `The "dev" script will use concurrently API internally and automatically set a port ${chalk.whiteBright(`"vovk dev --next-dev"`)}`,
217
+ value: 'IMPLICIT',
218
+ },
219
+ {
220
+ name: 'Yes, with explicit concurrently',
221
+ value: 'EXPLICIT',
222
+ description: `The "dev" script will use pre-defined PORT variable ${chalk.whiteBright(`"PORT=3000 concurrently 'vovk dev' 'next dev' --kill-others"`)}`,
223
+ },
224
+ {
225
+ name: 'No',
226
+ value: null,
227
+ description: 'Add the scripts manually',
228
+ },
229
+ ],
230
+ });
231
+ if (devScriptType) {
232
+ const updateNpmScriptsAction = new UpdateNpmScriptsAction(context);
233
+ context.actions.push(updateNpmScriptsAction);
234
+ updateNpmScriptsAction.data.useConcurrently = devScriptType === 'EXPLICIT';
235
+ }
236
+ if (!(await UpdateTsconfigAction.checkTsconfigForExperimentalDecorators()) &&
237
+ (await confirm({ message: 'Do you want to add experimentalDecorators to tsconfig.json?' }))) {
238
+ const updateTsconfigAction = new UpdateTsconfigAction(context);
239
+ context.actions.push(updateTsconfigAction);
240
+ }
241
+ await installDependencies(root, toBeInstalled, ['concurrently', 'vovk-cli']);
242
+ }
243
+ }
244
+ export const Init = Context;
@@ -0,0 +1 @@
1
+ export default function installDependencies(installDir: string, dependencies: string[], devDependencies: string[]): Promise<void>;
@@ -0,0 +1,28 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import * as path from 'path';
4
+ const execPromise = promisify(exec);
5
+ export default async function installDependencies(installDir, dependencies, devDependencies) {
6
+ const fullPath = path.resolve(installDir);
7
+ try {
8
+ if (dependencies.length > 0) {
9
+ console.log(`Installing dependencies in ${fullPath}...`);
10
+ const { stdout, stderr } = await execPromise(`npm install ${dependencies.join(' ')} --prefix ${fullPath}`);
11
+ console.log(stdout);
12
+ if (stderr) {
13
+ console.error(stderr);
14
+ }
15
+ }
16
+ if (devDependencies.length > 0) {
17
+ console.log(`Installing dev dependencies in ${fullPath}...`);
18
+ const { stdout, stderr } = await execPromise(`npm install --save-dev ${devDependencies.join(' ')} --prefix ${fullPath}`);
19
+ console.log(stdout);
20
+ if (stderr) {
21
+ console.error(stderr);
22
+ }
23
+ }
24
+ }
25
+ catch (err) {
26
+ console.error(`Error installing dependencies: ${err.message}`);
27
+ }
28
+ }
package/dist/types.d.mts CHANGED
@@ -13,7 +13,7 @@ export type VovkEnv = {
13
13
  VOVK_API_ENTRY_POINT?: string;
14
14
  VOVK_ROOT_SEGMENT_MODULES_DIR_NAME?: string;
15
15
  VOVK_LOG_LEVEL?: LogLevelNames;
16
- __VOVK_START_SERVER_IN_STANDALONE_MODE__?: 'true';
16
+ __VOVK_START_WATCHER_IN_STANDALONE_MODE__?: 'true';
17
17
  };
18
18
  export type VovkConfig = {
19
19
  clientOutDir?: string;
@@ -0,0 +1 @@
1
+ export default function formatLoggedSegmentName(segmentName: string, withChalk?: boolean): string;
@@ -0,0 +1,5 @@
1
+ import chalk from 'chalk';
2
+ export default function formatLoggedSegmentName(segmentName, withChalk = false) {
3
+ const text = segmentName ? `segment "${segmentName}"` : 'the root segment';
4
+ return withChalk ? chalk.white.bold(text) : text;
5
+ }
@@ -0,0 +1,8 @@
1
+ import loglevel, { type LogLevelNames } from 'loglevel';
2
+ export default function getLogger(level: LogLevelNames): {
3
+ info: (msg: string) => void;
4
+ warn: (msg: string) => void;
5
+ error: (msg: string) => void;
6
+ debug: (msg: string) => void;
7
+ raw: loglevel.RootLogger;
8
+ };
@@ -0,0 +1,13 @@
1
+ import chalk from 'chalk';
2
+ import loglevel from 'loglevel';
3
+ export default function getLogger(level) {
4
+ const log = {
5
+ info: (msg) => loglevel.info(chalk.cyanBright(`🐺 ${msg}`)),
6
+ warn: (msg) => loglevel.warn(chalk.yellowBright(`🐺 ${msg}`)),
7
+ error: (msg) => loglevel.error(chalk.redBright(`🐺 ${msg}`)),
8
+ debug: (msg) => loglevel.debug(chalk.gray(`🐺 ${msg}`)),
9
+ raw: loglevel,
10
+ };
11
+ loglevel.setLevel(level);
12
+ return log;
13
+ }
@@ -2,6 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import debounce from 'lodash/debounce.js';
4
4
  import writeOneSchemaFile, { ROOT_SEGMENT_SCHEMA_NAME } from './writeOneSchemaFile.mjs';
5
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
5
6
  export default async function ensureSchemaFiles(schemaOutFullPath, segmentNames, projectInfo) {
6
7
  const now = Date.now();
7
8
  let hasChanged = false;
@@ -30,7 +31,7 @@ export default segmentSchema;`;
30
31
  skipIfExists: true,
31
32
  });
32
33
  if (isCreated) {
33
- projectInfo?.log.debug(`Created empty schema file for segment "${segmentName}"`);
34
+ projectInfo?.log.debug(`Created empty schema file for ${formatLoggedSegmentName(segmentName, true)}`);
34
35
  hasChanged = true;
35
36
  }
36
37
  }));
@@ -56,7 +57,7 @@ export default segmentSchema;`;
56
57
  if (!segmentNames.includes(segmentName) &&
57
58
  !segmentNames.includes(segmentName.replace(ROOT_SEGMENT_SCHEMA_NAME, ''))) {
58
59
  await fs.unlink(fullPath);
59
- projectInfo?.log.debug(`Deleted unnecessary schema file for segment "${segmentName}"`);
60
+ projectInfo?.log.debug(`Deleted unnecessary schema file for ${formatLoggedSegmentName(segmentName, true)}`);
60
61
  hasChanged = true;
61
62
  }
62
63
  }
@@ -0,0 +1,6 @@
1
+ export declare class VovkCLIWatcher {
2
+ #private;
3
+ start({ clientOutDir }?: {
4
+ clientOutDir?: string;
5
+ }): Promise<void>;
6
+ }
@@ -5,12 +5,13 @@ import path from 'path';
5
5
  import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
6
6
  import writeOneSchemaFile from './writeOneSchemaFile.mjs';
7
7
  import logDiffResult from './logDiffResult.mjs';
8
- import generateClient from './generateClient.mjs';
8
+ import generateClient from '../generateClient.mjs';
9
9
  import locateSegments from '../locateSegments.mjs';
10
10
  import debounceWithArgs from '../utils/debounceWithArgs.mjs';
11
11
  import debounce from 'lodash/debounce.js';
12
12
  import isEmpty from 'lodash/isEmpty.js';
13
- export class VovkCLIServer {
13
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
14
+ export class VovkCLIWatcher {
14
15
  #projectInfo;
15
16
  #segments = [];
16
17
  #schemas = {};
@@ -45,7 +46,7 @@ export class VovkCLIServer {
45
46
  .on('change', (filePath) => {
46
47
  log.debug(`File ${filePath} has been changed at segments folder`);
47
48
  if (segmentReg.test(filePath)) {
48
- void this.#ping(getSegmentName(filePath));
49
+ void this.#requestSchema(getSegmentName(filePath));
49
50
  }
50
51
  })
51
52
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -53,7 +54,7 @@ export class VovkCLIServer {
53
54
  log.debug(`Directory ${dirPath} has been added to segments folder`);
54
55
  this.#segments = await locateSegments(apiDirFullPath);
55
56
  for (const { segmentName } of this.#segments) {
56
- void this.#ping(segmentName);
57
+ void this.#requestSchema(segmentName);
57
58
  }
58
59
  })
59
60
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -61,7 +62,7 @@ export class VovkCLIServer {
61
62
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
62
63
  this.#segments = await locateSegments(apiDirFullPath);
63
64
  for (const { segmentName } of this.#segments) {
64
- void this.#ping(segmentName);
65
+ void this.#requestSchema(segmentName);
65
66
  }
66
67
  })
67
68
  .on('unlink', (filePath) => {
@@ -104,12 +105,12 @@ export class VovkCLIServer {
104
105
  })
105
106
  .on('addDir', () => {
106
107
  for (const { segmentName } of this.#segments) {
107
- void this.#ping(segmentName);
108
+ void this.#requestSchema(segmentName);
108
109
  }
109
110
  })
110
111
  .on('unlinkDir', () => {
111
112
  for (const { segmentName } of this.#segments) {
112
- void this.#ping(segmentName);
113
+ void this.#requestSchema(segmentName);
113
114
  }
114
115
  })
115
116
  .on('ready', () => {
@@ -177,18 +178,18 @@ export class VovkCLIServer {
177
178
  if (affectedSegments.length) {
178
179
  log.debug(`A file with controller or worker ${namesOfClasses.join(', ')} have been modified at path "${filePath}". Segment(s) affected: ${affectedSegments.map((s) => s.segmentName).join(', ')}`);
179
180
  for (const segment of affectedSegments) {
180
- await this.#ping(segment.segmentName);
181
+ await this.#requestSchema(segment.segmentName);
181
182
  }
182
183
  }
183
184
  }
184
185
  };
185
- #ping = debounceWithArgs(async (segmentName) => {
186
+ #requestSchema = debounceWithArgs(async (segmentName) => {
186
187
  const { apiEntryPoint, log, port } = this.#projectInfo;
187
188
  const endpoint = `${apiEntryPoint.startsWith('http') ? apiEntryPoint : `http://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
188
- log.debug(`Pinging segment "${segmentName}" at ${endpoint}`);
189
+ log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName, true)} at ${endpoint}`);
189
190
  const resp = await fetch(endpoint);
190
191
  if (resp.status !== 200) {
191
- log.warn(`Ping to segment "${segmentName}" failed with status code ${resp.status}. Expected 200.`);
192
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName, true)} failed with status code ${resp.status}. Expected 200.`);
192
193
  return;
193
194
  }
194
195
  let schema = null;
@@ -196,7 +197,7 @@ export class VovkCLIServer {
196
197
  ({ schema } = (await resp.json()));
197
198
  }
198
199
  catch (error) {
199
- log.error(`Error parsing schema for segment "${segmentName}": ${error.message}`);
200
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName, true)}: ${error.message}`);
200
201
  }
201
202
  await this.#handleSchema(schema);
202
203
  }, 500);
@@ -224,18 +225,18 @@ export class VovkCLIServer {
224
225
  const timeTook = Date.now() - now;
225
226
  if (diffResult) {
226
227
  logDiffResult(segment.segmentName, diffResult, this.#projectInfo);
227
- log.info(`Schema for segment "${schema.segmentName}" has been updated in ${timeTook}ms`);
228
+ log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName, true)} has been updated in ${timeTook}ms`);
228
229
  }
229
230
  }
230
231
  else if (schema && (!isEmpty(schema.controllers) || !isEmpty(schema.workers))) {
231
- log.error(`Non-empty schema provided for segment "${schema.segmentName}" but emitSchema is false`);
232
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName, true)} but emitSchema is false`);
232
233
  }
233
234
  if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
234
235
  log.debug(`All segments with emitSchema=true have schema.`);
235
236
  await generateClient(this.#projectInfo, this.#segments, this.#schemas);
236
237
  }
237
238
  }
238
- async startServer({ clientOutDir } = {}) {
239
+ async start({ clientOutDir } = {}) {
239
240
  this.#projectInfo = await getProjectInfo({ clientOutDir });
240
241
  const { log, config, cwd, apiDir } = this.#projectInfo;
241
242
  process.on('uncaughtException', (err) => {
@@ -248,16 +249,16 @@ export class VovkCLIServer {
248
249
  const schemaOutFullPath = path.join(cwd, config.schemaOutDir);
249
250
  this.#segments = await locateSegments(apiDirFullPath);
250
251
  await debouncedEnsureSchemaFiles(schemaOutFullPath, this.#segments.map((s) => s.segmentName), this.#projectInfo);
251
- // Ping every segment in 3 seconds in order to update schema and start watching
252
+ // Request schema every segment in 3 seconds in order to update schema and start watching
252
253
  setTimeout(() => {
253
254
  for (const { segmentName } of this.#segments) {
254
- void this.#ping(segmentName);
255
+ void this.#requestSchema(segmentName);
255
256
  }
256
257
  this.#watch();
257
258
  }, 3000);
258
259
  }
259
260
  }
260
261
  const env = process.env;
261
- if (env.__VOVK_START_SERVER_IN_STANDALONE_MODE__ === 'true') {
262
- void new VovkCLIServer().startServer();
262
+ if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
263
+ void new VovkCLIWatcher().start();
263
264
  }
@@ -1,4 +1,5 @@
1
1
  import chalk from 'chalk';
2
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
2
3
  export default function logDiffResult(segmentName, diffResult, projectInfo) {
3
4
  const diffNormalized = [];
4
5
  diffResult.workers.added.forEach((name) => {
@@ -30,43 +31,43 @@ export default function logDiffResult(segmentName, diffResult, projectInfo) {
30
31
  case 'worker':
31
32
  switch (diffNormalizedItem.type) {
32
33
  case 'added':
33
- projectInfo.log.info(`New worker ${chalk.white.bold(diffNormalizedItem.name)} has been added to segment "${chalk.white.bold(segmentName)}"`);
34
+ projectInfo.log.info(`Schema for worker ${chalk.white.bold(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
34
35
  break;
35
36
  case 'removed':
36
- projectInfo.log.info(`Worker ${chalk.white.bold(diffNormalizedItem.name)} has been removed from segment "${chalk.white.bold(segmentName)}"`);
37
+ projectInfo.log.info(`Schema for worker ${chalk.white.bold(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
37
38
  break;
38
39
  }
39
40
  break;
40
41
  case 'controller':
41
42
  switch (diffNormalizedItem.type) {
42
43
  case 'added':
43
- projectInfo.log.info(`New controller ${chalk.white.bold(diffNormalizedItem.name)} has been added to segment "${chalk.white.bold(segmentName)}"`);
44
+ projectInfo.log.info(`Schema for controller ${chalk.white.bold(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
44
45
  break;
45
46
  case 'removed':
46
- projectInfo.log.info(`Controller ${chalk.white.bold(diffNormalizedItem.name)} has been removed from segment "${chalk.white.bold(segmentName)}"`);
47
+ projectInfo.log.info(`Schema for controller ${chalk.white.bold(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
47
48
  break;
48
49
  }
49
50
  break;
50
51
  case 'workerHandler':
51
52
  switch (diffNormalizedItem.type) {
52
53
  case 'added':
53
- projectInfo.log.info(`New worker method ${chalk.white.bold(diffNormalizedItem.name)} has been added in segment "${chalk.white.bold(segmentName)}"`);
54
+ projectInfo.log.info(`Schema for worker method ${chalk.white.bold(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
54
55
  break;
55
56
  case 'removed':
56
- projectInfo.log.info(`Worker method ${chalk.white.bold(diffNormalizedItem.name)} has been removed in segment "${chalk.white.bold(segmentName)}"`);
57
+ projectInfo.log.info(`Schema for worker method ${chalk.white.bold(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
57
58
  break;
58
59
  }
59
60
  break;
60
61
  case 'controllerHandler':
61
62
  switch (diffNormalizedItem.type) {
62
63
  case 'added':
63
- projectInfo.log.info(`New controller method ${chalk.white.bold(diffNormalizedItem.name)} has been added in segment "${chalk.white.bold(segmentName)}"`);
64
+ projectInfo.log.info(`Schema for controller method ${chalk.white.bold(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
64
65
  break;
65
66
  case 'removed':
66
- projectInfo.log.info(`Controller method ${chalk.white.bold(diffNormalizedItem.name)} has been removed in segment "${chalk.white.bold(segmentName)}"`);
67
+ projectInfo.log.info(`Schema for controller method ${chalk.white.bold(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
67
68
  break;
68
69
  case 'changed':
69
- projectInfo.log.info(`Controller method ${chalk.white.bold(diffNormalizedItem.name)} has been changed in segment "${chalk.white.bold(segmentName)}"`);
70
+ projectInfo.log.info(`Schema for controller method ${chalk.white.bold(diffNormalizedItem.name)} has been changed at ${formatLoggedSegmentName(segmentName, true)}`);
70
71
  break;
71
72
  }
72
73
  break;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-beta.12",
3
+ "version": "0.0.1-beta.14",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -34,11 +34,17 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "@inquirer/prompts": "^5.3.8",
37
+ "@npmcli/package-json": "^5.2.0",
37
38
  "chalk": "^5.3.0",
38
39
  "chokidar": "^3.6.0",
39
40
  "commander": "^12.1.0",
40
41
  "concurrently": "^8.2.2",
42
+ "jsonc-parser": "^3.3.1",
41
43
  "lodash": "^4.17.21",
42
44
  "loglevel": "^1.9.1"
45
+ },
46
+ "devDependencies": {
47
+ "@types/npmcli__package-json": "^4.0.4",
48
+ "type-fest": "^4.26.0"
43
49
  }
44
50
  }
package/dist/init.d.mts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/init.mjs DELETED
@@ -1,171 +0,0 @@
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
- import { confirm } from '@inquirer/prompts';
60
- // Or
61
- // import confirm from '@inquirer/confirm';
62
- // eslint-disable-next-line no-console
63
- void confirm({ message: 'Continue?' }).then(console.info);
64
- /*
65
- const wizard = [
66
- {
67
- description: 'Check if the project is already initialized',
68
- type: 'check',
69
- choices: {
70
- type: 'choice',
71
- choices: [
72
- {
73
- label: 'Yes',
74
- action: 'continue',
75
- },
76
- {
77
- label: 'No',
78
- action: 'exit',
79
- exitMessage: 'Exiting...',
80
- },
81
- ],
82
- },
83
- },
84
- {
85
- description: 'Check for package.json',
86
- type: 'check',
87
- handleCheck: () => {
88
- // Check if the project is already initialized
89
- },
90
- yes: {
91
- action: 'continue',
92
- },
93
- no: {
94
- action: 'exit',
95
- exitMessage: 'Exiting...',
96
- },
97
- },
98
- {
99
- description: 'Check for tsconfig.json',
100
- type: 'check',
101
- handleCheck: () => {
102
- // Check for package.json
103
- },
104
- yes: {
105
- action: 'continue',
106
- },
107
- no: {
108
- action: 'exit',
109
- exitMessage: 'Exiting...',
110
- },
111
- },
112
- {
113
- description: 'Check Next.js installed with app router',
114
- type: 'check',
115
- handleCheck: () => {
116
- // Check for tsconfig.json
117
- },
118
- yes: {
119
- action: 'continue',
120
- },
121
- no: {
122
- action: 'exit',
123
- exitMessage: 'Exiting...',
124
- },
125
- },
126
- {
127
- description: 'Choose validation library',
128
- type: 'install',
129
- choices: {
130
- type: 'choice',
131
- choices: [
132
- {
133
- package: 'vovk-zod',
134
- action: 'install',
135
- notes: 'Further installation notes: install zod',
136
- },
137
- {
138
- package: 'vovk-yup',
139
- action: 'install',
140
- notes: 'Further installation notes: install yup',
141
- },
142
- {
143
- package: 'vovk-dto',
144
- action: 'install',
145
- notes: 'Further installation notes: install class-validator and class-transformer',
146
- },
147
- {
148
- package: null,
149
- action: 'continue',
150
- },
151
- ],
152
- },
153
- },
154
- {
155
- description: 'Do you want to enable client validation?',
156
- type: 'choice',
157
- choices: [
158
- {
159
- label: 'Yes',
160
- action: 'updateConfig',
161
- },
162
- {
163
- label: 'No',
164
- action: 'updateConfig',
165
- },
166
- ],
167
- },
168
- ];
169
-
170
- // export default console.info(wizard);
171
- */
@@ -1,6 +0,0 @@
1
- export declare class VovkCLIServer {
2
- #private;
3
- startServer({ clientOutDir }?: {
4
- clientOutDir?: string;
5
- }): Promise<void>;
6
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes