vovk-cli 0.0.1-draft.332 → 0.0.1-draft.334

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.
@@ -16,10 +16,7 @@ const schema = {
16
16
  $schema: '<%- t.VovkSchemaIdEnum.SCHEMA %>',
17
17
  segments,
18
18
  meta: {
19
- apiRoot: '<%= t.apiRoot %>', // for debugging purposes
20
- <% if(t.isVovkProject) { %>
21
- ...meta
22
- <% } %>
19
+ <% if(t.isVovkProject) { %>...meta<% } %>
23
20
  }
24
21
  };
25
22
 
@@ -22,11 +22,7 @@ export const schema = {
22
22
  $schema: '<%- t.VovkSchemaIdEnum.SCHEMA %>',
23
23
  segments,
24
24
  meta: {
25
- $schema: '<%- t.VovkSchemaIdEnum.META %>',
26
- apiRoot: '<%= t.apiRoot %>', // for debugging purposes
27
- <% if(t.isVovkProject) { %>
28
- ...meta,
29
- <% } %>
25
+ <% if(t.isVovkProject) { %>...meta,<% } %>
30
26
  }
31
27
  };
32
28
 
@@ -17,9 +17,11 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
17
17
  throw new Error('No output directory specified for composed client');
18
18
  }
19
19
  const outDir = cliBundleOptions?.outDir ?? bundleConfig.tsdownBuildOptions.outDir;
20
+ const tsconfig = cliBundleOptions?.tsconfig ?? bundleConfig.tsdownBuildOptions.tsconfig;
20
21
  if (!outDir) {
21
22
  throw new Error('No output directory specified for bundling');
22
23
  }
24
+ const outDirAbsolute = path.resolve(cwd, outDir);
23
25
  await generate({
24
26
  isEnsuringClient: false,
25
27
  isBundle: true,
@@ -47,18 +49,20 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
47
49
  dts: true,
48
50
  format: ['cjs', 'esm'],
49
51
  fixedExtension: true,
50
- outDir,
52
+ clean: true,
51
53
  ...bundleConfig.tsdownBuildOptions,
54
+ outDir: outDirAbsolute,
55
+ tsconfig,
52
56
  });
53
- const outDirAbsolute = path.resolve(cwd, outDir);
54
57
  log.debug(`Bundled index.ts to ${chalkHighlightThing(outDirAbsolute)}`);
55
58
  await build({
56
59
  entry: path.join(tsFullClientOutAbsoluteDirInput, './schema.ts'),
57
60
  dts: true,
58
61
  format: ['cjs'],
59
62
  fixedExtension: true,
60
- outDir,
63
+ outDir: outDirAbsolute,
61
64
  clean: false,
65
+ tsconfig,
62
66
  });
63
67
  log.debug(`Bundled schema.ts to ${chalkHighlightThing(outDirAbsolute)}`);
64
68
  const requiresGroup = groupBy(Object.entries(bundleConfig.requires), ([, relativePath]) => relativePath);
@@ -74,7 +78,7 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
74
78
  cliGenerateOptions: {
75
79
  origin: cliBundleOptions?.origin ?? bundleConfig.origin,
76
80
  composedFrom: group.map(([templateName]) => templateName),
77
- composedOut: path.resolve(outDir, relativePath),
81
+ composedOut: path.resolve(outDirAbsolute, relativePath),
78
82
  composedOnly: true,
79
83
  },
80
84
  });
@@ -1,7 +1,7 @@
1
1
  import type { DevOptions } from '../types.mjs';
2
2
  export declare class VovkDev {
3
3
  #private;
4
- constructor({ schemaOut, devHttps }: Pick<DevOptions, 'schemaOut' | 'devHttps'>);
4
+ constructor({ schemaOut, devHttps, logLevel }: Pick<DevOptions, 'schemaOut' | 'devHttps' | 'logLevel'>);
5
5
  start({ exit }: {
6
6
  exit: boolean;
7
7
  }): Promise<void>;
@@ -38,9 +38,11 @@ export class VovkDev {
38
38
  #onFirstTimeGenerate = null;
39
39
  #schemaOut = null;
40
40
  #devHttps;
41
- constructor({ schemaOut, devHttps }) {
42
- this.#schemaOut = schemaOut ?? null;
43
- this.#devHttps = devHttps ?? false;
41
+ #logLevel;
42
+ constructor({ schemaOut, devHttps, logLevel }) {
43
+ this.#schemaOut = schemaOut || null;
44
+ this.#devHttps = devHttps || false;
45
+ this.#logLevel = logLevel || 'info';
44
46
  }
45
47
  #watchSegments = (callback) => {
46
48
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
@@ -165,7 +167,7 @@ export class VovkDev {
165
167
  let isInitial = true;
166
168
  let isReady = false;
167
169
  const handle = debounce(async () => {
168
- this.#projectInfo = await getProjectInfo();
170
+ this.#projectInfo = await getProjectInfo({ logLevel: this.#logLevel });
169
171
  const { config, apiDirAbsolutePath } = this.#projectInfo;
170
172
  this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
171
173
  await this.#modulesWatcher?.close();
@@ -323,7 +325,7 @@ export class VovkDev {
323
325
  }
324
326
  async start({ exit }) {
325
327
  const now = Date.now();
326
- this.#projectInfo = await getProjectInfo();
328
+ this.#projectInfo = await getProjectInfo({ logLevel: this.#logLevel });
327
329
  const { log, config, cwd, apiDirAbsolutePath } = this.#projectInfo;
328
330
  this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
329
331
  log.info('Starting...');
@@ -392,6 +394,7 @@ if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
392
394
  void new VovkDev({
393
395
  schemaOut: env.__VOVK_SCHEMA_OUT_FLAG__ || undefined,
394
396
  devHttps: env.__VOVK_DEV_HTTPS_FLAG__ === 'true',
397
+ logLevel: env.__VOVK_LOG_LEVEL__,
395
398
  }).start({
396
399
  exit: env.__VOVK_EXIT__ === 'true',
397
400
  });
@@ -1,7 +1,9 @@
1
1
  import { VovkSchemaIdEnum, type VovkStrictConfig } from 'vovk';
2
- export default function getConfig({ configPath, cwd }: {
2
+ import type { LogLevelNames } from 'loglevel';
3
+ export default function getConfig({ configPath, cwd, logLevel, }: {
3
4
  configPath?: string;
4
5
  cwd: string;
6
+ logLevel?: LogLevelNames;
5
7
  }): Promise<{
6
8
  config: VovkStrictConfig;
7
9
  srcRoot: string | null;
@@ -71,7 +73,7 @@ export default function getConfig({ configPath, cwd }: {
71
73
  excludeSegments?: string[];
72
74
  includeSegments?: never;
73
75
  });
74
- clientTemplateDefs?: Record<string, import("vovk/mjs/types").ClientTemplateDef>;
76
+ clientTemplateDefs?: Record<string, import("vovk/types").ClientTemplateDef>;
75
77
  imports?: {
76
78
  fetcher?: string | [string, string] | [string];
77
79
  validateOnClient?: string | [string, string] | [string];
@@ -106,8 +108,8 @@ export default function getConfig({ configPath, cwd }: {
106
108
  banner?: string;
107
109
  };
108
110
  apiRoot?: string;
109
- getModuleName?: "nestjs-operation-id" | (string & {}) | "api" | import("vovk/mjs/types").GetOpenAPINameFn;
110
- getMethodName?: "nestjs-operation-id" | "camel-case-operation-id" | "auto" | import("vovk/mjs/types").GetOpenAPINameFn;
111
+ getModuleName?: "nestjs-operation-id" | (string & {}) | "api" | import("vovk/types").GetOpenAPINameFn;
112
+ getMethodName?: "nestjs-operation-id" | "camel-case-operation-id" | "auto" | import("vovk/types").GetOpenAPINameFn;
111
113
  errorMessageKey?: string;
112
114
  };
113
115
  };
@@ -6,7 +6,7 @@ import getRelativeSrcRoot from './getRelativeSrcRoot.mjs';
6
6
  import getTemplateDefs, { BuiltInTemplateName } from './getTemplateDefs.mjs';
7
7
  import { normalizeOpenAPIMixins } from '../../utils/normalizeOpenAPIMixins.mjs';
8
8
  import chalkHighlightThing from '../../utils/chalkHighlightThing.mjs';
9
- export default async function getConfig({ configPath, cwd }) {
9
+ export default async function getConfig({ configPath, cwd, logLevel, }) {
10
10
  const { configAbsolutePaths, error, userConfig } = await getUserConfig({
11
11
  configPath,
12
12
  cwd,
@@ -65,7 +65,7 @@ export default async function getConfig({ configPath, cwd }) {
65
65
  origin: (env.VOVK_ORIGIN ?? conf.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
66
66
  rootEntry: env.VOVK_ROOT_ENTRY ?? conf.rootEntry ?? 'api',
67
67
  rootSegmentModulesDirName: conf.rootSegmentModulesDirName ?? '',
68
- logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'info',
68
+ logLevel: conf.logLevel ?? 'info',
69
69
  devHttps: conf.devHttps ?? false,
70
70
  moduleTemplates: {
71
71
  service: 'vovk-cli/module-templates/service.ts.ejs',
@@ -85,7 +85,7 @@ export default async function getConfig({ configPath, cwd }) {
85
85
  else if (Array.isArray(conf.emitConfig)) {
86
86
  config.emitConfig = conf.emitConfig;
87
87
  } // else it's false and emitConfig already is []
88
- const log = getLogger(config.logLevel);
88
+ const log = getLogger(logLevel ?? config.logLevel);
89
89
  config.openApiMixins = await normalizeOpenAPIMixins({
90
90
  mixinModules: conf.openApiMixins ?? {},
91
91
  cwd,
@@ -1,9 +1,11 @@
1
+ import { LogLevelNames } from 'loglevel';
1
2
  export type ProjectInfo = Awaited<ReturnType<typeof getProjectInfo>>;
2
- export default function getProjectInfo({ port: givenPort, cwd, configPath, srcRootRequired, }?: {
3
+ export default function getProjectInfo({ port: givenPort, cwd, configPath, srcRootRequired, logLevel, }?: {
3
4
  port?: number;
4
5
  cwd?: string;
5
6
  configPath?: string;
6
7
  srcRootRequired?: boolean;
8
+ logLevel?: LogLevelNames;
7
9
  }): Promise<{
8
10
  cwd: string;
9
11
  port: string;
@@ -13,7 +15,7 @@ export default function getProjectInfo({ port: givenPort, cwd, configPath, srcRo
13
15
  vovkCliPackage: {
14
16
  version: string;
15
17
  };
16
- config: import("vovk").VovkStrictConfig;
18
+ config: import("vovk/types").VovkStrictConfig;
17
19
  packageJson: import("type-fest").PackageJson;
18
20
  isNextInstalled: boolean;
19
21
  log: {
@@ -2,13 +2,16 @@ import path from 'node:path';
2
2
  import getConfig from './getConfig/index.mjs';
3
3
  import { getPackageJson } from '../utils/getPackageJson.mjs';
4
4
  import { readFile } from 'node:fs/promises';
5
- export default async function getProjectInfo({ port: givenPort, cwd = process.cwd(), configPath, srcRootRequired = true, } = {}) {
5
+ export default async function getProjectInfo({ port: givenPort, cwd = process.cwd(), configPath, srcRootRequired = true, logLevel, } = {
6
+ logLevel: 'info',
7
+ }) {
6
8
  const port = givenPort?.toString() ?? process.env.PORT ?? '3000';
7
9
  // Make PORT available to the config file at getConfig
8
10
  process.env.PORT = port;
9
11
  const { config, srcRoot, configAbsolutePaths, log } = await getConfig({
10
12
  configPath,
11
13
  cwd,
14
+ logLevel,
12
15
  });
13
16
  const packageJson = await getPackageJson(cwd, log);
14
17
  const isNextInstalled = !!packageJson?.dependencies?.next || !!packageJson?.devDependencies?.next;
package/dist/index.mjs CHANGED
@@ -25,8 +25,9 @@ program
25
25
  .option('--exit', 'kill the processes when schema and client is generated')
26
26
  .option('--schema-out <path>', 'path to schema output directory (default: .vovk-schema)')
27
27
  .option('--https, --dev-https', 'use HTTPS for the dev server (default: false)')
28
+ .option('--log-level <level>', 'set the log level')
28
29
  .action(async (nextArgs, options) => {
29
- const { nextDev, exit = false, schemaOut, devHttps } = options;
30
+ const { nextDev, exit = false, schemaOut, devHttps, logLevel } = options;
30
31
  const portAttempts = 30;
31
32
  const PORT = !nextDev
32
33
  ? process.env.PORT
@@ -52,9 +53,11 @@ program
52
53
  env: {
53
54
  PORT,
54
55
  __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true',
56
+ // TODO: Pass these as flags
55
57
  __VOVK_SCHEMA_OUT_FLAG__: schemaOut ?? '',
56
58
  __VOVK_DEV_HTTPS_FLAG__: devHttps ? 'true' : 'false',
57
59
  __VOVK_EXIT__: exit ? 'true' : 'false',
60
+ __VOVK_LOG_LEVEL__: logLevel ?? undefined,
58
61
  },
59
62
  },
60
63
  ], {
@@ -70,7 +73,7 @@ program
70
73
  }
71
74
  }
72
75
  else {
73
- void new VovkDev({ schemaOut, devHttps }).start({ exit });
76
+ void new VovkDev({ schemaOut, devHttps, logLevel }).start({ exit });
74
77
  }
75
78
  });
76
79
  program
@@ -98,8 +101,13 @@ program
98
101
  .option('--openapi-root-url <urls...>', 'root URLs corresponding to the index of --openapi option')
99
102
  .option('--openapi-mixin-name <names...>', 'mixin names corresponding to the index of --openapi option')
100
103
  .option('--openapi-fallback <paths...>', 'save OpenAPI spec and use it as a fallback if URL is not available')
104
+ .option('--log-level <level>', 'set the log level')
101
105
  .action(async (cliGenerateOptions) => {
102
- const projectInfo = await getProjectInfo({ configPath: cliGenerateOptions.configPath, srcRootRequired: false });
106
+ const projectInfo = await getProjectInfo({
107
+ configPath: cliGenerateOptions.configPath,
108
+ srcRootRequired: false,
109
+ logLevel: cliGenerateOptions.logLevel,
110
+ });
103
111
  await new VovkGenerate({
104
112
  projectInfo,
105
113
  forceNothingWrittenLog: true,
@@ -118,12 +126,18 @@ program
118
126
  .option('--config <config>', 'path to config file')
119
127
  .option('--schema <path>', 'path to schema folder (default: .vovk-schema)')
120
128
  .option('--origin <url>', 'set the origin URL for the generated client')
129
+ .option('--tsconfig <path>', 'path to tsconfig.json for bundling by tsdown')
121
130
  .option('--openapi, --openapi-spec <openapi_path_or_urls...>', 'use OpenAPI schema instead of Vovk schema')
122
131
  .option('--openapi-get-module-name <names...>', 'module names corresponding to the index of --openapi option')
123
132
  .option('--openapi-get-method-name <names...>', 'method names corresponding to the index of --openapi option')
124
133
  .option('--openapi-root-url <urls...>', 'root URLs corresponding to the index of --openapi option')
134
+ .option('--log-level <level>', 'set the log level')
125
135
  .action(async (cliBundleOptions) => {
126
- const projectInfo = await getProjectInfo({ configPath: cliBundleOptions.config, srcRootRequired: false });
136
+ const projectInfo = await getProjectInfo({
137
+ configPath: cliBundleOptions.config,
138
+ srcRootRequired: false,
139
+ logLevel: cliBundleOptions.logLevel,
140
+ });
127
141
  const { cwd, config, log, isNextInstalled } = projectInfo;
128
142
  const fullSchema = await getProjectFullSchema({
129
143
  schemaOutAbsolutePath: path.resolve(cwd, cliBundleOptions?.schema ?? config.schemaOutDir),
@@ -147,7 +161,10 @@ program
147
161
  .option('--no-segment-update', 'do not update segment files when creating a new module')
148
162
  .option('--dry-run', 'do not write files to disk')
149
163
  .option('--static', 'if the segment is static')
150
- .action((components, newOptions) => newComponents(components, newOptions));
164
+ .option('--log-level <level>', 'set the log level')
165
+ .action(async (components, newOptions) => newComponents(components, await getProjectInfo({
166
+ logLevel: newOptions.logLevel,
167
+ }), newOptions));
151
168
  program
152
169
  .command('help')
153
170
  .description('Show help message')
@@ -11,14 +11,36 @@ export default async function createConfig({ root, log, options: { validationLib
11
11
  .readFile(path.join(root, 'package.json'), 'utf-8')
12
12
  .then((content) => JSON.parse(content).type === 'module');
13
13
  const configAbsolutePath = path.join(dir, isModule ? 'vovk.config.mjs' : 'vovk.config.js');
14
+ const typeTemplates = {
15
+ controller: 'vovk-cli/module-templates/type/controller.ts.ejs',
16
+ service: 'vovk-cli/module-templates/type/service.ts.ejs',
17
+ };
14
18
  const moduleTemplates = {
15
- controller: 'vovk-cli/module-templates/controller.ts.ejs',
16
- service: 'vovk-cli/module-templates/service.ts.ejs',
19
+ ...typeTemplates,
20
+ ...{
21
+ type: typeTemplates,
22
+ zod: {
23
+ controller: 'vovk-zod/module-templates/controller.ts.ejs',
24
+ },
25
+ yup: {
26
+ controller: 'vovk-yup/module-templates/controller.ts.ejs',
27
+ },
28
+ 'class-validator': {
29
+ controller: 'vovk-dto/module-templates/controller.ts.ejs',
30
+ },
31
+ valibot: {
32
+ controller: 'vovk-cli/module-templates/valibot/controller.ts.ejs',
33
+ },
34
+ arktype: {
35
+ controller: 'vovk-cli/module-templates/arktype/controller.ts.ejs',
36
+ },
37
+ }[validationLibrary ?? 'type'],
17
38
  };
18
39
  config.imports ??= {};
19
- config.imports.validateOnClient = validationLibrary === 'vovk-dto' ? 'vovk-dto/validateOnClient' : 'vovk-ajv';
20
- if (validationLibrary) {
40
+ config.imports.validateOnClient = validationLibrary === 'class-validator' ? 'vovk-dto/validateOnClient' : 'vovk-ajv';
41
+ if (validationLibrary && !moduleTemplates) {
21
42
  try {
43
+ // TODO: Legacy, is it useful to keep it?
22
44
  const validationTemplates = await getTemplateFilesFromPackage(validationLibrary, channel);
23
45
  Object.assign(moduleTemplates, validationTemplates);
24
46
  }
@@ -0,0 +1,4 @@
1
+ export declare function createStandardSchemaValidatorFile({ cwd, validationLibrary, }: {
2
+ cwd: string;
3
+ validationLibrary: 'arktype' | 'valibot';
4
+ }): Promise<void>;
@@ -0,0 +1,38 @@
1
+ import fs from 'fs/promises';
2
+ import getRelativeSrcRoot from '../getProjectInfo/getConfig/getRelativeSrcRoot.mjs';
3
+ import path from 'path';
4
+ function getCode(validationLibrary) {
5
+ if (validationLibrary === 'valibot') {
6
+ return `
7
+ import { createStandardValidation, KnownAny } from 'vovk';
8
+ import { toJsonSchema } from '@valibot/to-json-schema';
9
+ import * as v from 'valibot';
10
+
11
+ const withValibot = createStandardValidation({
12
+ toJSONSchema: (model: v.BaseSchema<KnownAny, KnownAny, KnownAny>) => toJsonSchema(model),
13
+ });
14
+
15
+ export default withValibot;
16
+ `.trimStart();
17
+ }
18
+ if (validationLibrary === 'arktype') {
19
+ return `
20
+ import { createStandardValidation } from 'vovk';
21
+ import { type } from 'arktype';
22
+
23
+ const withArk = createStandardValidation({
24
+ toJSONSchema: (model: type) => model.toJsonSchema(),
25
+ });
26
+
27
+ export default withArk;
28
+ `.trimStart();
29
+ }
30
+ throw new Error(`Unknown validation library: ${validationLibrary}`);
31
+ }
32
+ export async function createStandardSchemaValidatorFile({ cwd, validationLibrary, }) {
33
+ const code = getCode(validationLibrary);
34
+ const srcRoot = (await getRelativeSrcRoot({ cwd })) ?? '.';
35
+ const libDir = path.join(cwd, srcRoot, 'lib');
36
+ await fs.mkdir(libDir, { recursive: true });
37
+ await fs.writeFile(path.join(libDir, `${validationLibrary === 'arktype' ? 'withArk' : 'withValibot'}.ts`), code);
38
+ }
@@ -8,18 +8,28 @@ import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
8
8
  import installDependencies, { getPackageManager } from './installDependencies.mjs';
9
9
  import getLogger from '../utils/getLogger.mjs';
10
10
  import createConfig from './createConfig.mjs';
11
- import updateNPMScripts, { getBuildScript, getDevScript } from './updateNPMScripts.mjs';
11
+ import updateNPMScripts, { getDevScript } from './updateNPMScripts.mjs';
12
12
  import checkTSConfigForExperimentalDecorators from './checkTSConfigForExperimentalDecorators.mjs';
13
13
  import updateTypeScriptConfig from './updateTypeScriptConfig.mjs';
14
14
  import updateDependenciesWithoutInstalling from './updateDependenciesWithoutInstalling.mjs';
15
15
  import logUpdateDependenciesError from './logUpdateDependenciesError.mjs';
16
16
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
17
+ import { createStandardSchemaValidatorFile } from './createStandardSchemaValidatorFile.mjs';
17
18
  export class Init {
18
19
  root;
19
20
  log;
20
- async #init({ configPaths, pkgJson, }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, lang, dryRun, channel, }) {
21
+ async #init({ configPaths, pkgJson, cwd = process.cwd(), }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, lang, dryRun, channel, }) {
21
22
  const { log, root } = this;
22
- const dependencies = ['vovk', 'vovk-client', 'vovk-ajv', 'openapi3-ts'];
23
+ const dependencies = [
24
+ 'vovk',
25
+ 'vovk-client',
26
+ 'openapi3-ts',
27
+ 'vovk-ajv',
28
+ 'ajv',
29
+ 'ajv-errors',
30
+ 'ajv-formats',
31
+ 'ajv-i18n',
32
+ ];
23
33
  const devDependencies = ['vovk-cli'];
24
34
  if (lang?.includes('py')) {
25
35
  devDependencies.push('vovk-python');
@@ -33,9 +43,18 @@ export class Init {
33
43
  log.debug(`Deleted existing config file${configPaths.length > 1 ? 's' : ''} at ${configPaths.join(', ')}`);
34
44
  }
35
45
  if (validationLibrary) {
36
- dependencies.push(validationLibrary, ...({
37
- 'vovk-zod': ['zod'],
38
- 'vovk-dto': ['class-validator', 'class-transformer', 'dto-mapped-types', 'reflect-metadata'],
46
+ dependencies.push(...({
47
+ zod: ['zod', 'vovk-zod'],
48
+ 'class-validator': [
49
+ 'class-validator',
50
+ 'class-transformer',
51
+ 'dto-mapped-types',
52
+ 'reflect-metadata',
53
+ 'vovk-dto',
54
+ ],
55
+ yup: ['yup', 'vovk-yup'],
56
+ valibot: ['valibot', '@valibot/to-json-schema'],
57
+ arktype: ['arktype'],
39
58
  }[validationLibrary] ?? []));
40
59
  }
41
60
  if (updateScripts) {
@@ -56,7 +75,7 @@ export class Init {
56
75
  const compilerOptions = {
57
76
  experimentalDecorators: true,
58
77
  };
59
- if (validationLibrary === 'vovk-dto') {
78
+ if (validationLibrary === 'class-validator') {
60
79
  compilerOptions.emitDecoratorMetadata = true;
61
80
  }
62
81
  if (!dryRun)
@@ -120,6 +139,12 @@ export class Init {
120
139
  }
121
140
  }
122
141
  }
142
+ if (validationLibrary === 'valibot' || validationLibrary === 'arktype') {
143
+ createStandardSchemaValidatorFile({
144
+ cwd,
145
+ validationLibrary,
146
+ });
147
+ }
123
148
  try {
124
149
  const { configAbsolutePath } = await createConfig({
125
150
  root,
@@ -135,13 +160,13 @@ export class Init {
135
160
  async main({ prefix, yes, logLevel, useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, lang, dryRun, channel, }) {
136
161
  const cwd = process.cwd();
137
162
  const root = path.resolve(cwd, prefix ?? '.');
138
- const log = getLogger(logLevel);
163
+ const log = getLogger(logLevel ?? 'info');
139
164
  const pkgJson = await NPMCliPackageJson.load(root);
140
165
  this.root = root;
141
166
  this.log = log;
142
167
  const configPaths = await getConfigPaths({ cwd, relativePath: prefix });
143
168
  if (yes) {
144
- return this.#init({ configPaths, pkgJson }, {
169
+ return this.#init({ configPaths, pkgJson, cwd }, {
145
170
  prefix: prefix ?? '.',
146
171
  useNpm: useNpm ?? (!useYarn && !usePnpm && !useBun),
147
172
  useYarn: useYarn ?? false,
@@ -150,7 +175,7 @@ export class Init {
150
175
  skipInstall: skipInstall ?? false,
151
176
  updateTsConfig: updateTsConfig ?? true,
152
177
  updateScripts: updateScripts ?? 'implicit',
153
- validationLibrary: validationLibrary?.toLocaleLowerCase() === 'none' ? null : (validationLibrary ?? 'vovk-zod'),
178
+ validationLibrary: validationLibrary?.toLocaleLowerCase() === 'none' ? null : (validationLibrary ?? 'zod'),
154
179
  dryRun: dryRun ?? false,
155
180
  channel: channel ?? 'latest',
156
181
  lang: lang ?? [],
@@ -174,17 +199,27 @@ export class Init {
174
199
  : (validationLibrary ??
175
200
  (await select({
176
201
  message: 'Choose validation library',
177
- default: 'vovk-zod',
202
+ default: 'zod',
178
203
  choices: [
179
204
  {
180
- name: 'vovk-zod',
181
- value: 'vovk-zod',
205
+ name: 'Zod',
206
+ value: 'zod',
182
207
  description: 'Use Zod for data validation',
183
208
  },
184
209
  {
185
- name: 'vovk-dto',
186
- value: 'vovk-dto',
187
- description: 'Use class-validator for data validation. Also installs class-transformer, dto-mapped-types and reflect-metadata',
210
+ name: 'class-validator',
211
+ value: 'class-validator',
212
+ description: 'Use class-validator for data validation',
213
+ },
214
+ {
215
+ name: 'Valibot',
216
+ value: 'valibot',
217
+ description: 'Use valibot for data validation.',
218
+ },
219
+ {
220
+ name: 'ArkType',
221
+ value: 'arktype',
222
+ description: 'Use arktype for data validation.',
188
223
  },
189
224
  { name: 'None', value: null, description: 'Install validation library later' },
190
225
  ],
@@ -196,12 +231,12 @@ export class Init {
196
231
  {
197
232
  name: 'Yes, use "concurrently" implicitly',
198
233
  value: 'implicit',
199
- description: `The "dev" script will use "concurrently" API to run "next dev" and "vovk dev" commands together and automatically find an available port ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'implicit')}"`)} and the "build" scrilt will run "vovk generate" before "next build" ${chalk.whiteBright.bold(`"${getBuildScript(pkgJson)}"`)}`,
234
+ description: `The "dev" script will use "concurrently" API to run "next dev" and "vovk dev" commands together and automatically find an available port ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'implicit')}"`)} and the "prebuild" script will run "vovk generate"`,
200
235
  },
201
236
  {
202
237
  name: 'Yes, use "concurrently" explicitly',
203
238
  value: 'explicit',
204
- description: `The "dev" script will use pre-defined PORT variable and run "next dev" and "vovk dev" as "concurrently" CLI arguments ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'explicit')}"`)} and the "build" scrilt will run "vovk generate" before "next build" ${chalk.whiteBright.bold(`"${getBuildScript(pkgJson)}"`)}`,
239
+ description: `The "dev" script will use pre-defined PORT variable and run "next dev" and "vovk dev" as "concurrently" CLI arguments ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'explicit')}"`)} and the "prebuild" script will run "vovk generate"`,
205
240
  },
206
241
  {
207
242
  name: 'No',
@@ -220,7 +255,7 @@ export class Init {
220
255
  }
221
256
  if (shouldAsk) {
222
257
  const keys = ['experimentalDecorators'];
223
- if (validationLibrary === 'vovk-dto') {
258
+ if (validationLibrary === 'class-validator') {
224
259
  keys.push('emitDecoratorMetadata');
225
260
  }
226
261
  updateTsConfig = await confirm({
@@ -229,13 +264,13 @@ export class Init {
229
264
  }
230
265
  }
231
266
  lang ??= await checkbox({
232
- message: 'Do you want to generate RPC client for other languages besides TypeScript (beta)?',
267
+ message: 'Do you want to generate RPC client for other languages besides TypeScript (experimental)?',
233
268
  choices: [
234
269
  { name: 'Python', value: 'py' },
235
270
  { name: 'Rust', value: 'rs' },
236
271
  ],
237
272
  });
238
- await this.#init({ configPaths, pkgJson }, {
273
+ await this.#init({ configPaths, pkgJson, cwd }, {
239
274
  useNpm: useNpm ?? (!useYarn && !usePnpm && !useBun),
240
275
  useYarn: useYarn ?? false,
241
276
  usePnpm: usePnpm ?? false,
@@ -1,4 +1,3 @@
1
1
  import NPMCliPackageJson from '@npmcli/package-json';
2
2
  export declare function getDevScript(pkgJson: NPMCliPackageJson, updateScriptsMode: 'implicit' | 'explicit'): string;
3
- export declare function getBuildScript(pkgJson: NPMCliPackageJson): string;
4
3
  export default function updateNPMScripts(pkgJson: NPMCliPackageJson, root: string, updateScriptsMode: 'implicit' | 'explicit'): Promise<void>;
@@ -5,16 +5,12 @@ export function getDevScript(pkgJson, updateScriptsMode) {
5
5
  ? `PORT=3000 concurrently '${nextDev}' 'vovk dev' --kill-others`
6
6
  : `vovk dev --next-dev${nextDevFlags ? ` -- ${nextDevFlags}` : ''}`;
7
7
  }
8
- export function getBuildScript(pkgJson) {
9
- const nextBuild = pkgJson.content.scripts?.build ?? 'next build';
10
- return `vovk generate && ${nextBuild}`;
11
- }
12
8
  export default async function updateNPMScripts(pkgJson, root, updateScriptsMode) {
13
9
  pkgJson.update({
14
10
  scripts: {
15
11
  ...pkgJson.content.scripts,
16
12
  dev: getDevScript(pkgJson, updateScriptsMode),
17
- build: getBuildScript(pkgJson),
13
+ prebuild: 'vovk generate',
18
14
  },
19
15
  });
20
16
  await pkgJson.save();
@@ -14,7 +14,7 @@ export function initProgram(program) {
14
14
  .option('--update-ts-config', 'update tsconfig.json')
15
15
  .option('--update-scripts <mode>', 'update package.json scripts ("implicit" or "explicit")')
16
16
  .option('--lang <languages...>', 'generate client for other programming languages by default ("py" for Python and "rs" for Rust are supported)')
17
- .option('--validation-library <library>', 'validation library to use ("vovk-zod", "vovk-dto" or another); set to "none" to skip')
17
+ .option('--validation-library <library>', 'validation library to use ("zod", "class-validator", "valibot", "arktype" or another); set to "none" to skip')
18
18
  .option('--channel <channel>', 'channel to use for fetching packages', 'latest')
19
19
  .option('--dry-run', 'do not write files to disk')
20
20
  .action((options) => new Init().main(options));
@@ -1,2 +1,3 @@
1
1
  import type { NewOptions } from '../types.mjs';
2
- export declare function newComponents(components: string[], { dryRun, dir, templates, overwrite, noSegmentUpdate, empty, static: isStaticSegment }: NewOptions): Promise<void>;
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mts';
3
+ export declare function newComponents(components: string[], projectInfo: ProjectInfo, { dryRun, dir, templates, overwrite, noSegmentUpdate, empty, static: isStaticSegment }: NewOptions): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import newModule from './newModule.mjs';
2
2
  import newSegment from './newSegment.mjs';
3
- export async function newComponents(components, { dryRun, dir, templates, overwrite, noSegmentUpdate, empty, static: isStaticSegment }) {
3
+ export async function newComponents(components, projectInfo, { dryRun, dir, templates, overwrite, noSegmentUpdate, empty, static: isStaticSegment }) {
4
4
  if (components[0] === 'segment' || components[0] === 'segments') {
5
5
  // vovk new segment [segmentName]
6
6
  let segmentNames = components
@@ -10,7 +10,7 @@ export async function newComponents(components, { dryRun, dir, templates, overwr
10
10
  segmentNames = [''];
11
11
  }
12
12
  for (const segmentName of segmentNames) {
13
- await newSegment({ segmentName, isStaticSegment, overwrite, dryRun });
13
+ await newSegment({ projectInfo, segmentName, isStaticSegment, overwrite, dryRun });
14
14
  }
15
15
  }
16
16
  else {
@@ -24,6 +24,7 @@ export async function newComponents(components, { dryRun, dir, templates, overwr
24
24
  throw new Error('A module name with an optional segment cannot be empty');
25
25
  }
26
26
  await newModule({
27
+ projectInfo,
27
28
  what,
28
29
  moduleNameWithOptionalSegment,
29
30
  dir,
@@ -1,4 +1,6 @@
1
- export default function newModule({ what, moduleNameWithOptionalSegment, dryRun, dir: dirFlag, templates: templatesFlag, noSegmentUpdate, overwrite, empty, }: {
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function newModule({ projectInfo, what, moduleNameWithOptionalSegment, dryRun, dir: dirFlag, templates: templatesFlag, noSegmentUpdate, overwrite, empty, }: {
3
+ projectInfo: ProjectInfo;
2
4
  what: string[];
3
5
  moduleNameWithOptionalSegment: string;
4
6
  dryRun?: boolean;
@@ -3,7 +3,6 @@ import fs from 'node:fs/promises';
3
3
  import { getTsconfig } from 'get-tsconfig';
4
4
  import render from './render.mjs';
5
5
  import addClassToSegmentCode from './addClassToSegmentCode.mjs';
6
- import getProjectInfo from '../getProjectInfo/index.mjs';
7
6
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
8
7
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
9
8
  import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
@@ -20,8 +19,8 @@ function splitByLast(str, delimiter = '/') {
20
19
  const after = str.substring(index + delimiter.length);
21
20
  return [before, after];
22
21
  }
23
- export default async function newModule({ what, moduleNameWithOptionalSegment, dryRun, dir: dirFlag, templates: templatesFlag, noSegmentUpdate, overwrite, empty, }) {
24
- const { config, log, cwd, apiDirAbsolutePath } = await getProjectInfo();
22
+ export default async function newModule({ projectInfo, what, moduleNameWithOptionalSegment, dryRun, dir: dirFlag, templates: templatesFlag, noSegmentUpdate, overwrite, empty, }) {
23
+ const { config, log, cwd, apiDirAbsolutePath } = projectInfo;
25
24
  const segments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
26
25
  const isNodeNextResolution = ['node16', 'nodenext'].includes((await getTsconfig(cwd)?.config?.compilerOptions?.moduleResolution?.toLowerCase()) ?? '');
27
26
  let templates = config.moduleTemplates;
@@ -1,4 +1,6 @@
1
- export default function newSegment({ segmentName, isStaticSegment, overwrite, dryRun, }: {
1
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ export default function newSegment({ projectInfo, segmentName, isStaticSegment, overwrite, dryRun, }: {
3
+ projectInfo: ProjectInfo;
2
4
  segmentName: string;
3
5
  isStaticSegment?: boolean;
4
6
  overwrite?: boolean;
@@ -1,13 +1,12 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
- import getProjectInfo from '../getProjectInfo/index.mjs';
4
3
  import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
5
4
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
6
5
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
7
6
  import prettify from '../utils/prettify.mjs';
8
7
  import chalk from 'chalk';
9
- export default async function newSegment({ segmentName, isStaticSegment, overwrite, dryRun, }) {
10
- const { apiDirAbsolutePath, log, config } = await getProjectInfo();
8
+ export default async function newSegment({ projectInfo, segmentName, isStaticSegment, overwrite, dryRun, }) {
9
+ const { apiDirAbsolutePath, log, config } = projectInfo;
11
10
  if (!apiDirAbsolutePath) {
12
11
  throw new Error('No API directory found. Please ensure you are in a Nest.js project.');
13
12
  }
package/dist/types.d.mts CHANGED
@@ -12,6 +12,7 @@ export interface DevOptions {
12
12
  nextDev?: boolean;
13
13
  exit?: boolean;
14
14
  devHttps?: boolean;
15
+ logLevel?: LogLevelNames;
15
16
  }
16
17
  export interface GenerateOptions {
17
18
  prettify?: boolean;
@@ -35,22 +36,24 @@ export interface GenerateOptions {
35
36
  segmentedOnly?: boolean;
36
37
  segmentedIncludeSegments?: string[];
37
38
  segmentedExcludeSegments?: string[];
39
+ logLevel?: LogLevelNames;
38
40
  }
39
41
  export interface BundleOptions extends Partial<Pick<VovkStrictConfig['bundle'], 'prebundleOutDir' | 'keepPrebundleDir' | 'includeSegments' | 'excludeSegments'>> {
40
42
  config?: string;
41
43
  schema?: string;
42
44
  outDir?: string;
43
45
  origin?: string;
46
+ tsconfig?: string;
44
47
  openapiSpec?: string[];
45
48
  openapiGetModuleName?: string[];
46
49
  openapiGetMethodName?: string[];
47
50
  openapiRootUrl?: string[];
48
51
  openapiMixinName?: string[];
52
+ logLevel?: LogLevelNames;
49
53
  }
50
54
  export interface InitOptions {
51
55
  prefix?: string;
52
56
  yes?: boolean;
53
- logLevel: LogLevelNames;
54
57
  useNpm?: boolean;
55
58
  useYarn?: boolean;
56
59
  usePnpm?: boolean;
@@ -58,10 +61,11 @@ export interface InitOptions {
58
61
  skipInstall?: boolean;
59
62
  updateTsConfig?: boolean;
60
63
  updateScripts?: 'implicit' | 'explicit';
61
- validationLibrary?: string | null;
64
+ validationLibrary?: 'zod' | 'yup' | 'class-validator' | 'valibot' | 'arktype' | null;
62
65
  dryRun?: boolean;
63
66
  lang?: string[];
64
67
  channel?: 'latest' | 'beta' | 'draft';
68
+ logLevel?: LogLevelNames;
65
69
  }
66
70
  export interface NewOptions {
67
71
  dryRun?: boolean;
@@ -71,6 +75,7 @@ export interface NewOptions {
71
75
  noSegmentUpdate?: boolean;
72
76
  empty?: boolean;
73
77
  static?: boolean;
78
+ logLevel?: LogLevelNames;
74
79
  }
75
80
  export type VovkEnv = {
76
81
  PORT?: string;
@@ -78,9 +83,9 @@ export type VovkEnv = {
78
83
  VOVK_ORIGIN?: string;
79
84
  VOVK_ROOT_ENTRY?: string;
80
85
  VOVK_API_ENTRY_POINT?: string;
81
- VOVK_LOG_LEVEL?: LogLevelNames;
82
86
  __VOVK_START_WATCHER_IN_STANDALONE_MODE__?: 'true';
83
87
  __VOVK_SCHEMA_OUT_FLAG__?: string;
84
88
  __VOVK_DEV_HTTPS_FLAG__?: 'true' | 'false';
85
89
  __VOVK_EXIT__?: 'true' | 'false';
90
+ __VOVK_LOG_LEVEL__?: LogLevelNames;
86
91
  };
@@ -0,0 +1,68 @@
1
+ <% const vars = {
2
+ ModuleName: t.TheThing + 'Controller',
3
+ ServiceName: t.TheThing + 'Service',
4
+ }; %>
5
+ ---
6
+ dir: <%= t.defaultDir %>
7
+ fileName: <%= vars.ModuleName + '.ts' %>
8
+ sourceName: <%= vars.ModuleName %>
9
+ compiledName: <%= t.TheThing + 'RPC' %>
10
+ ---
11
+
12
+ import { prefix, get, put, post, del, openapi } from 'vovk';
13
+ import { type } from 'arktype';
14
+ import withArk from '@/lib/withArk<%= t.nodeNextResolutionExt.ts %>';
15
+ <% if(t.withService) { %>
16
+ import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
17
+ <% } %>
18
+
19
+ @prefix('<%= t['the-things'] %>')
20
+ export default class <%= vars.ModuleName %> {
21
+ @openapi({
22
+ summary: 'Get <%= t.TheThings %>',
23
+ })
24
+ @get()
25
+ static get<%= t.TheThings %> = withArk({
26
+ query: type({ search: type('string') }),
27
+ handle(req) {
28
+ const search = req.nextUrl.searchParams.get('search');
29
+ <% if(t.withService) { %>
30
+ return <%= vars.ServiceName %>.get<%= t.TheThings %>(search);
31
+ <% } else { %>
32
+ return { results: [], search };
33
+ <% } %>
34
+ }
35
+ });
36
+
37
+ @openapi({
38
+ summary: 'Update <%= t.TheThing %>',
39
+ })
40
+ @put('{id}')
41
+ static update<%= t.TheThing %> = withArk({
42
+ body: type({
43
+ foo: type('"bar" | "baz"'),
44
+ }),
45
+ query: type({ q: type('string') }),
46
+ params: type({ id: type('string') }),
47
+ async handle(req, params) {
48
+ const { id } = params;
49
+ const body = await req.json();
50
+ const q = req.nextUrl.searchParams.get('q');
51
+ <% if(t.withService) { %>
52
+ return <%= vars.ServiceName %>.update<%= t.TheThing %>(id, q, body);
53
+ <% } else { %>
54
+ return { id, body, q };
55
+ <% } %>
56
+ }
57
+ });
58
+
59
+ @post()
60
+ static create<%= t.TheThing %> = () => {
61
+ // ...
62
+ };
63
+
64
+ @del(':id')
65
+ static delete<%= t.TheThing %> = () => {
66
+ // ...
67
+ };
68
+ }
@@ -0,0 +1,68 @@
1
+ <% const vars = {
2
+ ModuleName: t.TheThing + 'Controller',
3
+ ServiceName: t.TheThing + 'Service',
4
+ }; %>
5
+ ---
6
+ dir: <%= t.defaultDir %>
7
+ fileName: <%= vars.ModuleName + '.ts' %>
8
+ sourceName: <%= vars.ModuleName %>
9
+ compiledName: <%= t.TheThing + 'RPC' %>
10
+ ---
11
+
12
+ import { prefix, get, put, post, del, openapi } from 'vovk';
13
+ import * as v from 'valibot';
14
+ import withValibot from '@/lib/withValibot<%= t.nodeNextResolutionExt.ts %>';
15
+ <% if(t.withService) { %>
16
+ import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
17
+ <% } %>
18
+
19
+ @prefix('<%= t['the-things'] %>')
20
+ export default class <%= vars.ModuleName %> {
21
+ @openapi({
22
+ summary: 'Get <%= t.TheThings %>',
23
+ })
24
+ @get()
25
+ static get<%= t.TheThings %> = withValibot({
26
+ query: v.object({ search: v.string() }),
27
+ handle(req) {
28
+ const search = req.nextUrl.searchParams.get('search');
29
+ <% if(t.withService) { %>
30
+ return <%= vars.ServiceName %>.get<%= t.TheThings %>(search);
31
+ <% } else { %>
32
+ return { results: [], search };
33
+ <% } %>
34
+ }
35
+ });
36
+
37
+ @openapi({
38
+ summary: 'Update <%= t.TheThing %>',
39
+ })
40
+ @put('{id}')
41
+ static update<%= t.TheThing %> = withValibot({
42
+ body: v.object({
43
+ foo: v.union([v.literal('bar'), v.literal('baz')]),
44
+ }),
45
+ query: v.object({ q: v.string() }),
46
+ params: v.object({ id: v.string() }),
47
+ async handle(req, params) {
48
+ const { id } = params;
49
+ const body = await req.json();
50
+ const q = req.nextUrl.searchParams.get('q');
51
+ <% if(t.withService) { %>
52
+ return <%= vars.ServiceName %>.update<%= t.TheThing %>(id, q, body);
53
+ <% } else { %>
54
+ return { id, body, q };
55
+ <% } %>
56
+ }
57
+ });
58
+
59
+ @post()
60
+ static create<%= t.TheThing %> = () => {
61
+ // ...
62
+ };
63
+
64
+ @del(':id')
65
+ static delete<%= t.TheThing %> = () => {
66
+ // ...
67
+ };
68
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.332",
3
+ "version": "0.0.1-draft.334",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "homepage": "https://vovk.dev",
37
37
  "peerDependencies": {
38
- "vovk": "^3.0.0-draft.366"
38
+ "vovk": "^3.0.0-draft.396"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.31"