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

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 (32) hide show
  1. package/client-templates/readme/README.md.ejs +3 -2
  2. package/dist/bundle/index.mjs +2 -6
  3. package/dist/dev/ensureSchemaFiles.d.mts +1 -1
  4. package/dist/dev/index.mjs +20 -14
  5. package/dist/dev/writeMetaJson.d.mts +1 -1
  6. package/dist/dev/writeMetaJson.mjs +5 -2
  7. package/dist/generate/generate.d.mts +2 -5
  8. package/dist/generate/generate.mjs +38 -40
  9. package/dist/generate/getClientTemplateFiles.mjs +1 -1
  10. package/dist/generate/getProjectFullSchema.d.mts +5 -2
  11. package/dist/generate/getProjectFullSchema.mjs +6 -7
  12. package/dist/generate/index.mjs +3 -1
  13. package/dist/generate/writeOneClientFile.d.mts +4 -3
  14. package/dist/generate/writeOneClientFile.mjs +11 -5
  15. package/dist/getProjectInfo/getConfig/getTemplateDefs.mjs +3 -3
  16. package/dist/getProjectInfo/getConfig/index.d.mts +6 -49
  17. package/dist/getProjectInfo/getConfig/index.mjs +17 -14
  18. package/dist/getProjectInfo/getMetaSchema.d.mts +10 -0
  19. package/dist/getProjectInfo/getMetaSchema.mjs +13 -0
  20. package/dist/getProjectInfo/index.mjs +1 -1
  21. package/dist/index.mjs +3 -1
  22. package/dist/init/index.mjs +1 -10
  23. package/dist/types.d.mts +4 -0
  24. package/dist/utils/generateFnName.d.mts +23 -0
  25. package/dist/utils/generateFnName.mjs +76 -0
  26. package/dist/utils/normalizeOpenAPIMixin.d.mts +14 -0
  27. package/dist/utils/normalizeOpenAPIMixin.mjs +114 -0
  28. package/package.json +2 -2
  29. package/dist/generate/mergePackages.d.mts +0 -7
  30. package/dist/generate/mergePackages.mjs +0 -55
  31. package/dist/utils/normalizeOpenAPIMixins.d.mts +0 -7
  32. package/dist/utils/normalizeOpenAPIMixins.mjs +0 -67
@@ -3,13 +3,13 @@
3
3
 
4
4
  # <%= t.package.name %> v<%= t.package.version %> [![TypeScript](https://badgen.net/badge/-/TypeScript?icon=typescript&label&labelColor=blue&color=555555)](https://www.typescriptlang.org/) [![Vovk.ts](https://badgen.net/badge/Built%20with/Vovk.ts/333333?icon=https://vovk.dev/icon-white.svg)](https://vovk.dev)
5
5
 
6
- <%- t.package.description ? `> ${t.package.description}` : '' %>
6
+ <%- t.readme.description ?? (`${t.package.description ? `> ${t.package.description}` : ''}`) %>
7
7
 
8
8
  <%- t.package.license ? `License: **${t.package.license}**` : '' %>
9
9
 
10
10
  ```bash
11
11
  # Install the package
12
- npm install <%= t.package.name %>
12
+ <%= t.readme.installCommand ?? `npm install ${t.package.name}` %>
13
13
  ```
14
14
 
15
15
  <% Object.entries(t.schema.segments).forEach(([segmentName, segment]) => {
@@ -31,6 +31,7 @@ npm install <%= t.package.name %>
31
31
  handlerName,
32
32
  controllerSchema,
33
33
  package: t.package,
34
+ config: t.snippets,
34
35
  }).ts %>
35
36
  ```
36
37
  <% }) %>
@@ -29,10 +29,8 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
29
29
  forceNothingWrittenLog: true,
30
30
  fullSchema,
31
31
  locatedSegments,
32
- package: bundleConfig.package,
33
- readme: bundleConfig.readme,
34
32
  cliGenerateOptions: {
35
- origin: cliBundleOptions?.origin ?? bundleConfig.origin,
33
+ origin: cliBundleOptions?.origin,
36
34
  openapiSpec: cliBundleOptions?.openapiSpec,
37
35
  openapiGetModuleName: cliBundleOptions?.openapiGetModuleName,
38
36
  openapiGetMethodName: cliBundleOptions?.openapiGetMethodName,
@@ -73,10 +71,8 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
73
71
  forceNothingWrittenLog: true,
74
72
  fullSchema,
75
73
  locatedSegments,
76
- package: bundleConfig.package,
77
- readme: bundleConfig.readme,
78
74
  cliGenerateOptions: {
79
- origin: cliBundleOptions?.origin ?? bundleConfig.origin,
75
+ origin: cliBundleOptions?.origin,
80
76
  composedFrom: group.map(([templateName]) => templateName),
81
77
  composedOut: path.resolve(outDirAbsolute, relativePath),
82
78
  composedOnly: true,
@@ -2,5 +2,5 @@ import { ProjectInfo } from '../getProjectInfo/index.mjs';
2
2
  /**
3
3
  * Ensure that the schema files are created to avoid any import errors.
4
4
  */
5
- export default function ensureSchemaFiles(projectInfo: ProjectInfo | null, schemaOutAbsolutePath: string, segmentNames: string[]): Promise<void>;
5
+ export default function ensureSchemaFiles(projectInfo: ProjectInfo, schemaOutAbsolutePath: string, segmentNames: string[]): Promise<void>;
6
6
  export declare const debouncedEnsureSchemaFiles: import("lodash").DebouncedFunc<typeof ensureSchemaFiles>;
@@ -19,19 +19,11 @@ import debounceWithArgs from '../utils/debounceWithArgs.mjs';
19
19
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
20
20
  import writeMetaJson from './writeMetaJson.mjs';
21
21
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
22
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
22
23
  export class VovkDev {
23
24
  #projectInfo;
24
25
  #segments = [];
25
- #fullSchema = {
26
- $schema: VovkSchemaIdEnum.SCHEMA,
27
- segments: {},
28
- meta: {
29
- $schema: VovkSchemaIdEnum.META,
30
- config: {
31
- $schema: VovkSchemaIdEnum.CONFIG,
32
- },
33
- },
34
- };
26
+ #schemaSegments = {};
35
27
  #isWatching = false;
36
28
  #modulesWatcher = null;
37
29
  #segmentWatcher = null;
@@ -235,7 +227,7 @@ export class VovkDev {
235
227
  const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
236
228
  if (importRegex.test(code) && namesOfClasses.length) {
237
229
  const affectedSegments = this.#segments.filter((s) => {
238
- const segmentSchema = this.#fullSchema.segments[s.segmentName];
230
+ const segmentSchema = this.#schemaSegments[s.segmentName];
239
231
  if (!segmentSchema)
240
232
  return false;
241
233
  const controllersByOriginalName = keyBy(segmentSchema.controllers, 'originalControllerName');
@@ -287,7 +279,21 @@ export class VovkDev {
287
279
  }
288
280
  return { isError: false };
289
281
  }, 500);
290
- #generate = debounce(() => generate({ projectInfo: this.#projectInfo, fullSchema: this.#fullSchema, locatedSegments: this.#segments }).then(this.#onFirstTimeGenerate), 1000);
282
+ #generate = debounce(() => {
283
+ const fullSchema = {
284
+ $schema: VovkSchemaIdEnum.SCHEMA,
285
+ segments: this.#schemaSegments,
286
+ meta: getMetaSchema({
287
+ config: this.#projectInfo.config,
288
+ package: this.#projectInfo.packageJson,
289
+ }),
290
+ };
291
+ return generate({
292
+ projectInfo: this.#projectInfo,
293
+ fullSchema,
294
+ locatedSegments: this.#segments,
295
+ }).then(this.#onFirstTimeGenerate);
296
+ }, 1000);
291
297
  async #handleSegmentSchema(segmentName, segmentSchema) {
292
298
  const { log, config, cwd } = this.#projectInfo;
293
299
  if (!segmentSchema) {
@@ -301,7 +307,7 @@ export class VovkDev {
301
307
  log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
302
308
  return;
303
309
  }
304
- this.#fullSchema.segments[segmentName] = segmentSchema;
310
+ this.#schemaSegments[segmentName] = segmentSchema;
305
311
  if (segmentSchema.emitSchema) {
306
312
  const now = Date.now();
307
313
  const { diffResult } = await writeOneSegmentSchemaFile({
@@ -318,7 +324,7 @@ export class VovkDev {
318
324
  else if (segmentSchema && !isEmpty(segmentSchema.controllers)) {
319
325
  log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
320
326
  }
321
- if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
327
+ if (this.#segments.every((s) => this.#schemaSegments[s.segmentName])) {
322
328
  log.debug(`All segments with "emitSchema" have schema.`);
323
329
  this.#generate();
324
330
  }
@@ -1,2 +1,2 @@
1
1
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- export default function writeMetaJson(schemaOutAbsolutePath: string, projectInfo: ProjectInfo | null): Promise<void>;
2
+ export default function writeMetaJson(schemaOutAbsolutePath: string, projectInfo: ProjectInfo): Promise<void>;
@@ -1,11 +1,14 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import pick from 'lodash/pick.js';
4
3
  import { META_FILE_NAME } from './writeOneSegmentSchemaFile.mjs';
5
4
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
5
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
6
6
  export default async function writeMetaJson(schemaOutAbsolutePath, projectInfo) {
7
7
  const metaJsonPath = path.join(schemaOutAbsolutePath, META_FILE_NAME + '.json');
8
- const metaStr = JSON.stringify({ config: projectInfo ? pick(projectInfo.config, [...projectInfo.config.emitConfig, '$schema']) : {} }, null, 2);
8
+ const metaStr = JSON.stringify(getMetaSchema({
9
+ config: projectInfo.config,
10
+ package: projectInfo.packageJson,
11
+ }), null, 2);
9
12
  const existingStr = await fs.readFile(metaJsonPath, 'utf-8').catch(() => null);
10
13
  if (existingStr !== metaStr) {
11
14
  await fs.writeFile(metaJsonPath, metaStr);
@@ -1,9 +1,8 @@
1
- import { type VovkSchema, type VovkStrictConfig } from 'vovk';
2
- import type { PackageJson } from 'type-fest';
1
+ import { type VovkSchema } from 'vovk';
3
2
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
4
3
  import type { GenerateOptions } from '../types.mjs';
5
4
  import type { Segment } from '../locateSegments.mjs';
6
- export declare function generate({ isEnsuringClient, isBundle, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, package: argPackageJson, readme: argReadme, }: {
5
+ export declare function generate({ isEnsuringClient, isBundle, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, }: {
7
6
  isEnsuringClient?: boolean;
8
7
  isBundle?: boolean;
9
8
  projectInfo: ProjectInfo;
@@ -11,6 +10,4 @@ export declare function generate({ isEnsuringClient, isBundle, projectInfo, forc
11
10
  fullSchema: VovkSchema;
12
11
  locatedSegments: Segment[];
13
12
  cliGenerateOptions?: GenerateOptions;
14
- package?: PackageJson;
15
- readme?: VovkStrictConfig['bundle']['readme'];
16
13
  }): Promise<void>;
@@ -2,17 +2,16 @@ import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import matter from 'gray-matter';
4
4
  import _ from 'lodash';
5
- import { openAPIToVovkSchema } from 'vovk';
5
+ import { getGeneratorConfig, openAPIToVovkSchema, } from 'vovk';
6
6
  import getClientTemplateFiles from './getClientTemplateFiles.mjs';
7
7
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
8
8
  import pickSegmentFullSchema from '../utils/pickSegmentFullSchema.mjs';
9
9
  import removeUnlistedDirectories from '../utils/removeUnlistedDirectories.mjs';
10
10
  import getTemplateClientImports from './getTemplateClientImports.mjs';
11
- import mergePackages from './mergePackages.mjs';
12
11
  import writeOneClientFile, { normalizeOutTemplatePath } from './writeOneClientFile.mjs';
13
12
  import { ROOT_SEGMENT_FILE_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
14
13
  import { getTsconfig } from 'get-tsconfig';
15
- import { normalizeOpenAPIMixins } from '../utils/normalizeOpenAPIMixins.mjs';
14
+ import { normalizeOpenAPIMixin } from '../utils/normalizeOpenAPIMixin.mjs';
16
15
  import { BuiltInTemplateName } from '../getProjectInfo/getConfig/getTemplateDefs.mjs';
17
16
  const getIncludedSegmentNames = (config, fullSchema, configKey, cliGenerateOptions) => {
18
17
  const segments = Object.values(fullSchema.segments);
@@ -86,30 +85,35 @@ const cliOptionsToOpenAPIMixins = ({ openapiGetMethodName, openapiGetModuleName,
86
85
  },
87
86
  ]));
88
87
  };
89
- export async function generate({ isEnsuringClient = false, isBundle = false, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, package: argPackageJson, readme: argReadme, }) {
88
+ export async function generate({ isEnsuringClient = false, isBundle = false, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, }) {
90
89
  fullSchema = {
91
90
  ...fullSchema,
92
91
  // sort segments by name to avoid unnecessary rendering
93
- segments: Object.fromEntries(Object.entries(fullSchema.segments).sort(([a], [b]) => a.localeCompare(b))),
92
+ segments: Object.fromEntries(Object.entries(fullSchema.segments)
93
+ .sort(([a], [b]) => a.localeCompare(b))
94
+ // preserve original object, so segments can be extended
95
+ .map((segment) => ({ ...segment }))),
94
96
  };
95
- const { config, cwd, log, srcRoot, packageJson: rootPackageJson, vovkCliPackage } = projectInfo;
96
- const allOpenAPIMixins = {
97
- ...config.openApiMixins,
98
- ...cliOptionsToOpenAPIMixins(cliGenerateOptions ?? {}),
99
- };
100
- if (Object.keys(allOpenAPIMixins).length) {
101
- const mixins = Object.fromEntries(Object.entries(await normalizeOpenAPIMixins({ mixinModules: allOpenAPIMixins, log })).map(([mixinName, conf]) => [
102
- mixinName,
103
- openAPIToVovkSchema({ ...conf, mixinName }).segments[mixinName],
104
- ]));
105
- fullSchema = {
106
- ...fullSchema,
107
- segments: {
108
- ...fullSchema.segments,
109
- ...mixins,
97
+ const { config, cwd, log, srcRoot, vovkCliPackage } = projectInfo;
98
+ Object.entries(config.generatorConfig.segments ?? {}).forEach(([segmentName, segmentConfig]) => {
99
+ fullSchema.segments = {
100
+ ...fullSchema.segments,
101
+ [segmentName]: {
102
+ ...fullSchema.segments[segmentName],
103
+ ...openAPIToVovkSchema({ ...segmentConfig.openAPIMixin, segmentName }).segments[segmentName],
110
104
  },
111
105
  };
112
- }
106
+ });
107
+ const cliMixins = cliOptionsToOpenAPIMixins(cliGenerateOptions ?? {});
108
+ await Promise.all(Object.entries(cliMixins).map(async ([mixinName, mixinModule]) => {
109
+ fullSchema.segments = {
110
+ ...fullSchema.segments,
111
+ [mixinName]: {
112
+ ...fullSchema.segments[mixinName],
113
+ ...openAPIToVovkSchema(await normalizeOpenAPIMixin({ mixinModule, log })).segments[mixinName],
114
+ },
115
+ };
116
+ }));
113
117
  const isNodeNextResolution = ['node16', 'nodenext'].includes((await getTsconfig(cwd)?.config?.compilerOptions?.moduleResolution?.toLowerCase()) ?? '');
114
118
  const isVovkProject = !!srcRoot;
115
119
  const isComposedEnabled = cliGenerateOptions?.composedOnly ||
@@ -141,12 +145,11 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
141
145
  fullSchema,
142
146
  outCwdRelativeDir,
143
147
  });
144
- const packageJson = await mergePackages({
145
- log,
146
- rootPackageJson,
147
- packages: [config.composedClient.package, templateDef.composedClient?.package, argPackageJson],
148
+ const { package: packageJson, readme, origin, snippets, } = getGeneratorConfig({
149
+ schema: fullSchema,
150
+ config: templateDef.generatorConfig,
151
+ isBundle,
148
152
  });
149
- const readme = Object.assign({}, config.composedClient.readme, templateDef.composedClient?.readme, argReadme);
150
153
  const composedFullSchema = pickSegmentFullSchema(fullSchema, segmentNames);
151
154
  const hasMixins = Object.values(composedFullSchema.segments).some((segment) => segment.segmentType === 'mixin');
152
155
  if (templateName === BuiltInTemplateName.mixins && !hasMixins) {
@@ -164,6 +167,7 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
164
167
  matterResult,
165
168
  package: packageJson,
166
169
  readme,
170
+ snippets,
167
171
  isEnsuringClient,
168
172
  outCwdRelativeDir,
169
173
  templateDef,
@@ -173,7 +177,7 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
173
177
  isVovkProject,
174
178
  vovkCliPackage,
175
179
  isBundle,
176
- origin: cliGenerateOptions?.origin ?? templateDef.origin ?? config.origin,
180
+ origin: cliGenerateOptions?.origin ?? origin,
177
181
  });
178
182
  const outAbsoluteDir = path.resolve(cwd, outCwdRelativeDir);
179
183
  return {
@@ -220,19 +224,12 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
220
224
  outCwdRelativeDir,
221
225
  });
222
226
  const results = await Promise.all(segmentNames.map(async (segmentName) => {
223
- const packageJson = await mergePackages({
224
- log,
225
- rootPackageJson,
226
- packages: [
227
- fullSchema.segments[segmentName]?.meta?.package,
228
- config.segmentedClient.packages?.[segmentName],
229
- templateDef.segmentedClient?.packages?.[segmentName],
230
- argPackageJson,
231
- ],
227
+ const { package: packageJson, readme, origin, snippets, } = getGeneratorConfig({
228
+ schema: fullSchema,
229
+ config: templateDef.generatorConfig,
230
+ segmentName,
231
+ isBundle,
232
232
  });
233
- const readme = Object.assign({},
234
- // TODO fullSchema.segments[segmentName]?.meta?.readme,
235
- config.segmentedClient.readmes?.[segmentName], templateDef.segmentedClient?.readmes?.[segmentName], argReadme);
236
233
  const segmentedFullSchema = pickSegmentFullSchema(fullSchema, [segmentName]);
237
234
  const hasMixins = Object.values(segmentedFullSchema.segments).some((segment) => segment.segmentType === 'mixin');
238
235
  if (templateName === BuiltInTemplateName.mixins && !hasMixins) {
@@ -250,6 +247,7 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
250
247
  matterResult,
251
248
  package: packageJson,
252
249
  readme,
250
+ snippets,
253
251
  isEnsuringClient,
254
252
  outCwdRelativeDir,
255
253
  templateDef,
@@ -259,7 +257,7 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
259
257
  isVovkProject,
260
258
  vovkCliPackage,
261
259
  isBundle,
262
- origin: cliGenerateOptions?.origin ?? templateDef.origin ?? config.origin,
260
+ origin: cliGenerateOptions?.origin ?? origin,
263
261
  });
264
262
  return {
265
263
  written,
@@ -73,7 +73,7 @@ export default async function getClientTemplateFiles({ config, cwd, log, configK
73
73
  }
74
74
  def = {
75
75
  ...def,
76
- origin: templateDef.origin ?? def.origin,
76
+ generatorConfig: merge({}, templateDef?.generatorConfig, def.generatorConfig),
77
77
  composedClient: merge(omit(templateDef?.composedClient ?? {}, ['outDir']), def.composedClient),
78
78
  segmentedClient: merge(omit(templateDef?.segmentedClient ?? {}, ['outDir']), def.segmentedClient),
79
79
  };
@@ -1,7 +1,10 @@
1
- import { type VovkSchema } from 'vovk';
1
+ import { VovkStrictConfig, type VovkSchema } from 'vovk';
2
2
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
- export declare function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, }: {
3
+ import { PackageJson } from 'type-fest';
4
+ export declare function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, package: packageJson, config, }: {
4
5
  schemaOutAbsolutePath: string;
5
6
  isNextInstalled: boolean;
6
7
  log: ProjectInfo['log'];
8
+ package: PackageJson;
9
+ config: VovkStrictConfig;
7
10
  }): Promise<VovkSchema>;
@@ -3,16 +3,15 @@ import path from 'node:path';
3
3
  import { glob } from 'glob';
4
4
  import { VovkSchemaIdEnum } from 'vovk';
5
5
  import { META_FILE_NAME, ROOT_SEGMENT_FILE_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
6
- export async function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, }) {
6
+ import getMetaSchema from '../getProjectInfo/getMetaSchema.mjs';
7
+ export async function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, package: packageJson, config, }) {
7
8
  const result = {
8
9
  $schema: VovkSchemaIdEnum.SCHEMA,
9
10
  segments: {},
10
- meta: {
11
- $schema: VovkSchemaIdEnum.META,
12
- config: {
13
- $schema: VovkSchemaIdEnum.CONFIG,
14
- },
15
- },
11
+ meta: getMetaSchema({
12
+ config,
13
+ package: packageJson,
14
+ }),
16
15
  };
17
16
  const isEmptyLogOrWarn = isNextInstalled ? log.warn : log.debug;
18
17
  // Handle config.json
@@ -37,12 +37,14 @@ export class VovkGenerate {
37
37
  });
38
38
  }
39
39
  async getFullSchema() {
40
- const { log, config, cwd, isNextInstalled } = this.#projectInfo;
40
+ const { log, config, cwd, isNextInstalled, packageJson } = this.#projectInfo;
41
41
  const { schemaPath } = this.#cliGenerateOptions;
42
42
  const fullSchema = await getProjectFullSchema({
43
43
  schemaOutAbsolutePath: path.resolve(cwd, schemaPath ?? config.schemaOutDir),
44
44
  isNextInstalled,
45
45
  log,
46
+ package: packageJson,
47
+ config,
46
48
  });
47
49
  return fullSchema;
48
50
  }
@@ -1,11 +1,11 @@
1
- import { type VovkSchema, type VovkStrictConfig } from 'vovk';
1
+ import { VovkReadmeConfig, VovkSnippetsConfig, type VovkSchema, type VovkStrictConfig } from 'vovk';
2
2
  import type { PackageJson } from 'type-fest';
3
3
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
4
4
  import type { ClientTemplateFile } from './getClientTemplateFiles.mjs';
5
5
  import type { ClientImports } from './getTemplateClientImports.mjs';
6
6
  import type { Segment } from '../locateSegments.mjs';
7
7
  export declare function normalizeOutTemplatePath(out: string, packageJson: PackageJson): string;
8
- export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, isEnsuringClient, outCwdRelativeDir, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, vovkCliPackage, isBundle, origin, }: {
8
+ export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, snippets, isEnsuringClient, outCwdRelativeDir, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, vovkCliPackage, isBundle, origin, }: {
9
9
  cwd: string;
10
10
  projectInfo: ProjectInfo;
11
11
  clientTemplateFile: ClientTemplateFile;
@@ -21,7 +21,8 @@ export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFil
21
21
  content: string;
22
22
  };
23
23
  package: PackageJson;
24
- readme: VovkStrictConfig['bundle']['readme'];
24
+ readme: VovkReadmeConfig;
25
+ snippets: VovkSnippetsConfig;
25
26
  isEnsuringClient: boolean;
26
27
  outCwdRelativeDir: string;
27
28
  templateDef: VovkStrictConfig['clientTemplateDefs'][string];
@@ -2,7 +2,7 @@ import fs from 'fs/promises';
2
2
  import path from 'node:path';
3
3
  import ejs from 'ejs';
4
4
  import _ from 'lodash';
5
- import { createCodeExamples, VovkSchemaIdEnum } from 'vovk';
5
+ import { createCodeExamples, VovkSchemaIdEnum, } from 'vovk';
6
6
  import * as YAML from 'yaml';
7
7
  import TOML from '@iarna/toml';
8
8
  import prettify from '../utils/prettify.mjs';
@@ -11,7 +11,9 @@ import { compileJSONSchemaToTypeScriptType } from '../utils/compileJSONSchemaToT
11
11
  export function normalizeOutTemplatePath(out, packageJson) {
12
12
  return out.replace('[package_name]', packageJson.name?.replace(/-/g, '_') ?? 'my_package_name');
13
13
  }
14
- export default async function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, isEnsuringClient, outCwdRelativeDir, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, vovkCliPackage, isBundle, origin, }) {
14
+ export default async function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, snippets, isEnsuringClient, outCwdRelativeDir,
15
+ // templateDef,
16
+ locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, vovkCliPackage, isBundle, origin, }) {
15
17
  const { config, apiRoot } = projectInfo;
16
18
  const { templateFilePath, relativeDir } = clientTemplateFile;
17
19
  const locatedSegmentsByName = _.keyBy(locatedSegments, 'segmentName');
@@ -38,6 +40,7 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
38
40
  isVovkProject,
39
41
  package: packageJson,
40
42
  readme,
43
+ snippets,
41
44
  ROOT_SEGMENT_FILE_NAME,
42
45
  apiRoot: origin ? `${origin}/${config.rootEntry}` : apiRoot,
43
46
  imports,
@@ -63,11 +66,14 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
63
66
  ? path.relative(path.resolve(cwd, outCwdRelativeDir, typeof segmentName === 'string' ? segmentName || ROOT_SEGMENT_FILE_NAME : '.'), path.resolve(cwd, routeFilePath))
64
67
  : null;
65
68
  const segmentConfig = {
66
- ...(config.segmentConfig ? config.segmentConfig[sName] : {}),
67
- ...(templateDef.segmentConfig ? templateDef.segmentConfig[sName] : {}),
69
+ ...config.generatorConfig.segments?.[sName],
70
+ // ...templateDef.generatorConfig?.segments?.[sName],
68
71
  };
69
72
  const { origin: segmentConfigOrigin, rootEntry: segmentConfigRootEntry, segmentNameOverride } = segmentConfig;
70
- const reExports = { ...segmentConfig.reExports, ...(isBundle ? projectInfo.config.bundle.reExports : {}) };
73
+ const reExports = {
74
+ ...segmentConfig.reExports,
75
+ ...(isBundle ? projectInfo.config.bundle.generatorConfig?.reExports : {}),
76
+ };
71
77
  return [
72
78
  sName,
73
79
  {
@@ -129,9 +129,9 @@ export default function getTemplateDefs(userTemplateDefs = {}) {
129
129
  ...builtIn.segmentedClient,
130
130
  ...templateDef.segmentedClient,
131
131
  },
132
- segmentConfig: {
133
- ...builtIn.segmentConfig,
134
- ...templateDef.segmentConfig,
132
+ generatorConfig: {
133
+ ...builtIn.generatorConfig,
134
+ ...templateDef.generatorConfig,
135
135
  },
136
136
  // 'requires' and other props will be overridden
137
137
  };
@@ -14,14 +14,13 @@ export default function getConfig({ configPath, cwd, logLevel, }: {
14
14
  schemaOutDir?: string;
15
15
  modulesDir?: string;
16
16
  rootEntry?: string;
17
- origin?: string;
18
17
  logLevel?: "error" | "trace" | "debug" | "info" | "warn" | (string & {});
19
18
  libs?: {
20
19
  ajv: import("vovk").KnownAny;
21
20
  [key: string]: import("vovk").KnownAny;
22
21
  };
23
22
  devHttps?: boolean;
24
- composedClient?: ({
23
+ composedClient?: {
25
24
  enabled?: boolean;
26
25
  outDir?: string;
27
26
  fromTemplates?: string[];
@@ -32,13 +31,8 @@ export default function getConfig({ configPath, cwd, logLevel, }: {
32
31
  } | {
33
32
  excludeSegments?: string[];
34
33
  includeSegments?: never;
35
- })) & {
36
- package?: import("type-fest").PackageJson;
37
- readme?: {
38
- banner?: string;
39
- };
40
- };
41
- segmentedClient?: ({
34
+ });
35
+ segmentedClient?: {
42
36
  enabled?: boolean;
43
37
  outDir?: string;
44
38
  fromTemplates?: string[];
@@ -49,23 +43,13 @@ export default function getConfig({ configPath, cwd, logLevel, }: {
49
43
  } | {
50
44
  excludeSegments?: string[];
51
45
  includeSegments?: never;
52
- })) & {
53
- packages?: Record<string, import("type-fest").PackageJson>;
54
- readmes?: Record<string, {
55
- banner?: string;
56
- }>;
57
- };
46
+ });
58
47
  bundle?: {
59
48
  requires?: Record<string, string>;
60
49
  prebundleOutDir?: string;
61
50
  keepPrebundleDir?: boolean;
62
- origin?: string;
63
- package?: import("type-fest").PackageJson;
64
- readme?: {
65
- banner?: string;
66
- };
67
- reExports?: Record<string, string>;
68
51
  tsdownBuildOptions?: Parameters<typeof import("tsdown/config-9hj-APNF.mjs").build>[0];
52
+ generatorConfig: import("vovk").VovkGeneratorConfigCommon;
69
53
  } & ({
70
54
  excludeSegments?: never;
71
55
  includeSegments?: string[];
@@ -85,34 +69,7 @@ export default function getConfig({ configPath, cwd, logLevel, }: {
85
69
  controller?: string;
86
70
  [key: string]: string | undefined;
87
71
  };
88
- segmentConfig?: false | {
89
- [x: string]: {
90
- origin?: string;
91
- rootEntry?: string;
92
- segmentNameOverride?: string;
93
- reExports?: Record<string, string>;
94
- };
95
- };
96
- openApiMixins?: {
97
- [mixinName: string]: {
98
- source: {
99
- file: string;
100
- } | {
101
- url: string;
102
- fallback?: string;
103
- } | {
104
- object: import("openapi3-ts/oas31").OpenAPIObject;
105
- };
106
- package?: import("type-fest").PackageJson;
107
- readme?: {
108
- banner?: string;
109
- };
110
- apiRoot?: string;
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;
113
- errorMessageKey?: string;
114
- };
115
- };
72
+ generatorConfig?: import("vovk").VovkGeneratorConfig;
116
73
  } | null;
117
74
  log: {
118
75
  info: (msg: string) => void;
@@ -4,7 +4,7 @@ import getLogger from '../../utils/getLogger.mjs';
4
4
  import getUserConfig from './getUserConfig.mjs';
5
5
  import getRelativeSrcRoot from './getRelativeSrcRoot.mjs';
6
6
  import getTemplateDefs, { BuiltInTemplateName } from './getTemplateDefs.mjs';
7
- import { normalizeOpenAPIMixins } from '../../utils/normalizeOpenAPIMixins.mjs';
7
+ import { normalizeOpenAPIMixin } from '../../utils/normalizeOpenAPIMixin.mjs';
8
8
  import chalkHighlightThing from '../../utils/chalkHighlightThing.mjs';
9
9
  export default async function getConfig({ configPath, cwd, logLevel, }) {
10
10
  const { configAbsolutePaths, error, userConfig } = await getUserConfig({
@@ -12,6 +12,7 @@ export default async function getConfig({ configPath, cwd, logLevel, }) {
12
12
  cwd,
13
13
  });
14
14
  const conf = userConfig ?? {};
15
+ const log = getLogger(logLevel ?? conf.logLevel);
15
16
  const env = process.env;
16
17
  const clientTemplateDefs = getTemplateDefs(conf.clientTemplateDefs);
17
18
  const srcRoot = await getRelativeSrcRoot({ cwd });
@@ -51,9 +52,7 @@ export default async function getConfig({ configPath, cwd, logLevel, }) {
51
52
  [BuiltInTemplateName.readme]: '.',
52
53
  [BuiltInTemplateName.packageJson]: '.',
53
54
  },
54
- package: {},
55
- readme: {},
56
- reExports: {},
55
+ generatorConfig: {},
57
56
  ...conf.bundle,
58
57
  tsdownBuildOptions: {
59
58
  outDir: conf.bundle?.tsdownBuildOptions?.outDir ?? 'dist',
@@ -62,7 +61,6 @@ export default async function getConfig({ configPath, cwd, logLevel, }) {
62
61
  },
63
62
  modulesDir: conf.modulesDir ?? path.join(srcRoot ?? '.', 'modules'),
64
63
  schemaOutDir: env.VOVK_SCHEMA_OUT_DIR ?? conf.schemaOutDir ?? './.vovk-schema',
65
- origin: (env.VOVK_ORIGIN ?? conf.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
66
64
  rootEntry: env.VOVK_ROOT_ENTRY ?? conf.rootEntry ?? 'api',
67
65
  rootSegmentModulesDirName: conf.rootSegmentModulesDirName ?? '',
68
66
  logLevel: conf.logLevel ?? 'info',
@@ -73,11 +71,22 @@ export default async function getConfig({ configPath, cwd, logLevel, }) {
73
71
  ...conf.moduleTemplates,
74
72
  },
75
73
  libs: conf.libs ?? {},
76
- segmentConfig: conf.segmentConfig ?? {},
77
- openApiMixins: {},
74
+ generatorConfig: {
75
+ ...conf.generatorConfig,
76
+ origin: (env.VOVK_ORIGIN ?? conf?.generatorConfig?.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
77
+ segments: Object.fromEntries(await Promise.all(Object.entries(conf.generatorConfig?.segments ?? {}).map(async ([key, value]) => [
78
+ key,
79
+ {
80
+ ...value,
81
+ openAPIMixin: value.openAPIMixin
82
+ ? await normalizeOpenAPIMixin({ mixinModule: value.openAPIMixin, log, cwd })
83
+ : undefined,
84
+ },
85
+ ]))),
86
+ },
78
87
  };
79
88
  if (typeof conf.emitConfig === 'undefined') {
80
- config.emitConfig = ['libs'];
89
+ config.emitConfig = ['libs', 'generatorConfig'];
81
90
  }
82
91
  else if (conf.emitConfig === true) {
83
92
  config.emitConfig = Object.keys(config);
@@ -85,12 +94,6 @@ export default async function getConfig({ configPath, cwd, logLevel, }) {
85
94
  else if (Array.isArray(conf.emitConfig)) {
86
95
  config.emitConfig = conf.emitConfig;
87
96
  } // else it's false and emitConfig already is []
88
- const log = getLogger(logLevel ?? config.logLevel);
89
- config.openApiMixins = await normalizeOpenAPIMixins({
90
- mixinModules: conf.openApiMixins ?? {},
91
- cwd,
92
- log,
93
- });
94
97
  if (!userConfig) {
95
98
  log.warn(`Unable to load config at ${chalkHighlightThing(cwd)}. Using default values. ${error ?? ''}`);
96
99
  }
@@ -0,0 +1,10 @@
1
+ import { VovkSchemaIdEnum, VovkStrictConfig } from 'vovk';
2
+ import { PackageJson } from 'type-fest';
3
+ export default function getMetaSchema({ config, package: packageJson, }: {
4
+ config: VovkStrictConfig;
5
+ package: PackageJson;
6
+ }): {
7
+ config: VovkStrictConfig;
8
+ $schema: VovkSchemaIdEnum;
9
+ package: Pick<PackageJson, "name" | "version" | "description" | "author" | "license">;
10
+ };
@@ -0,0 +1,13 @@
1
+ import { VovkSchemaIdEnum } from 'vovk';
2
+ import pick from 'lodash/pick.js';
3
+ export default function getMetaSchema({ config, package: packageJson, }) {
4
+ return {
5
+ $schema: VovkSchemaIdEnum.META,
6
+ package: pick(packageJson, ['name', 'version', 'description', 'author', 'license']),
7
+ ...{
8
+ config: (config
9
+ ? pick(config, [...config.emitConfig, '$schema'])
10
+ : {}),
11
+ },
12
+ };
13
+ }
@@ -18,7 +18,7 @@ export default async function getProjectInfo({ port: givenPort, cwd = process.cw
18
18
  if (srcRootRequired && !srcRoot) {
19
19
  throw new Error(`Could not find app router directory at ${cwd}. Check Next.js docs for more info.`);
20
20
  }
21
- const apiRoot = `${config.origin ?? ''}/${config.rootEntry}`;
21
+ const apiRoot = `${config.generatorConfig.origin ?? ''}/${config.rootEntry}`;
22
22
  const apiDirAbsolutePath = srcRoot ? path.resolve(cwd, srcRoot, 'app', config.rootEntry) : null;
23
23
  if (configAbsolutePaths.length > 1) {
24
24
  log.warn(`Multiple config files found. Using the first one: ${configAbsolutePaths[0]}`);
package/dist/index.mjs CHANGED
@@ -138,11 +138,13 @@ program
138
138
  srcRootRequired: false,
139
139
  logLevel: cliBundleOptions.logLevel,
140
140
  });
141
- const { cwd, config, log, isNextInstalled } = projectInfo;
141
+ const { cwd, config, log, isNextInstalled, packageJson } = projectInfo;
142
142
  const fullSchema = await getProjectFullSchema({
143
143
  schemaOutAbsolutePath: path.resolve(cwd, cliBundleOptions?.schema ?? config.schemaOutDir),
144
144
  log,
145
145
  isNextInstalled,
146
+ package: packageJson,
147
+ config,
146
148
  });
147
149
  await bundle({
148
150
  projectInfo,
@@ -20,16 +20,7 @@ export class Init {
20
20
  log;
21
21
  async #init({ configPaths, pkgJson, cwd = process.cwd(), }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, lang, dryRun, channel, }) {
22
22
  const { log, root } = this;
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
+ const dependencies = ['vovk', 'vovk-client', 'openapi3-ts', 'vovk-ajv', 'ajv'];
33
24
  const devDependencies = ['vovk-cli'];
34
25
  if (lang?.includes('py')) {
35
26
  devDependencies.push('vovk-python');
package/dist/types.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { LogLevelNames } from 'loglevel';
2
+ import type { build } from 'tsdown';
2
3
  import type { VovkStrictConfig } from 'vovk';
3
4
  export type VovkModuleRenderResult = {
4
5
  fileName: string;
@@ -89,3 +90,6 @@ export type VovkEnv = {
89
90
  __VOVK_EXIT__?: 'true' | 'false';
90
91
  __VOVK_LOG_LEVEL__?: LogLevelNames;
91
92
  };
93
+ declare global {
94
+ var TSdownBuildOptions: Parameters<typeof build>[0];
95
+ }
@@ -0,0 +1,23 @@
1
+ import { HttpMethod } from 'vovk';
2
+ export interface VerbMapEntry {
3
+ noParams?: string;
4
+ withParams?: string;
5
+ default?: string;
6
+ }
7
+ export declare const VERB_MAP: Record<HttpMethod, VerbMapEntry>;
8
+ export declare function capitalize(str: string): string;
9
+ interface GenerateFnNameOptions {
10
+ /** Segments to strip out (e.g. ['api','v1']) */
11
+ ignoreSegments?: string[];
12
+ }
13
+ /**
14
+ * Turn an HTTP method + OpenAPI path into a camelCased function name.
15
+ *
16
+ * Examples:
17
+ * generateFnName('GET', '/users') // "listUsers"
18
+ * generateFnName('GET', '/users/{id}') // "getUsersById"
19
+ * generateFnName('POST', '/v1/api/orders') // "createOrders"
20
+ * generateFnName('PATCH', '/users/{userId}/profile') // "patchUsersProfileByUserId"
21
+ */
22
+ export declare function generateFnName(method: HttpMethod, rawPath: string, opts?: GenerateFnNameOptions): string;
23
+ export {};
@@ -0,0 +1,76 @@
1
+ export const VERB_MAP = {
2
+ GET: { noParams: 'list', withParams: 'get' },
3
+ POST: { default: 'create' },
4
+ PUT: { default: 'update' },
5
+ PATCH: { default: 'patch' },
6
+ DELETE: { default: 'delete' },
7
+ HEAD: { default: 'head' },
8
+ OPTIONS: { default: 'options' },
9
+ };
10
+ export function capitalize(str) {
11
+ if (str.length === 0)
12
+ return '';
13
+ return str[0].toUpperCase() + str.slice(1);
14
+ }
15
+ const DEFAULT_OPTIONS = {
16
+ ignoreSegments: ['api'],
17
+ };
18
+ /**
19
+ * Turn an HTTP method + OpenAPI path into a camelCased function name.
20
+ *
21
+ * Examples:
22
+ * generateFnName('GET', '/users') // "listUsers"
23
+ * generateFnName('GET', '/users/{id}') // "getUsersById"
24
+ * generateFnName('POST', '/v1/api/orders') // "createOrders"
25
+ * generateFnName('PATCH', '/users/{userId}/profile') // "patchUsersProfileByUserId"
26
+ */
27
+ export function generateFnName(method, rawPath, opts = {}) {
28
+ const { ignoreSegments } = {
29
+ ...DEFAULT_OPTIONS,
30
+ ...opts,
31
+ };
32
+ // 1. Clean & split path
33
+ const parts = rawPath
34
+ .replace(/^\/|\/$/g, '') // strip leading/trailing slash
35
+ .split('/')
36
+ .filter((seg) => !ignoreSegments?.includes(seg.toLowerCase()))
37
+ .filter(Boolean);
38
+ // 2. Separate resource tokens from path-params
39
+ const resources = [];
40
+ const params = [];
41
+ parts.forEach((seg) => {
42
+ const match = seg.match(/^{?([^}]+)}?$/);
43
+ if (match) {
44
+ params.push(match[1]);
45
+ }
46
+ else {
47
+ resources.push(seg);
48
+ }
49
+ });
50
+ // 3. Pick base verb from VERB_MAP
51
+ let baseVerb;
52
+ if (method === 'GET') {
53
+ baseVerb = params.length ? VERB_MAP.GET.withParams : VERB_MAP.GET.noParams;
54
+ }
55
+ else {
56
+ baseVerb = VERB_MAP[method].default;
57
+ }
58
+ // 4. Build the “resource” part
59
+ const resourcePart = resources.map(capitalize).join('');
60
+ // 5. Build the “ByParam” suffix
61
+ const byParams = params.length ? 'By' + params.map(capitalize).join('') : '';
62
+ // 6. Combine and ensure camelCase
63
+ const rawName = `${baseVerb}${resourcePart}${byParams}`;
64
+ return rawName[0].toLowerCase() + rawName.slice(1);
65
+ }
66
+ /*
67
+ // --- Example usage ---
68
+ console.log(generateFnName('GET', '/users')); // listUsers
69
+ console.log(generateFnName('GET', '/users/{id}')); // getUsersById
70
+ console.log(generateFnName('POST', '/users')); // createUsers
71
+ console.log(generateFnName('PATCH', '/users/{userId}/profile')); // patchUsersProfileByUserId
72
+ console.log(generateFnName('DELETE', '/v1/api/orders/{orderId}')); // deleteOrdersByOrderId
73
+
74
+ // You can also enable singularization:
75
+ console.log(generateFnName('GET', '/users/{userId}/orders', { singularizeResources: true })); // getUserOrderByUserId
76
+ */
@@ -0,0 +1,14 @@
1
+ import { HttpMethod, VovkOperationObject, VovkStrictConfig, type VovkConfig } from 'vovk';
2
+ import { OpenAPIObject } from 'openapi3-ts/oas31';
3
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
4
+ export type GetOpenAPINameFn = (config: {
5
+ operationObject: VovkOperationObject;
6
+ method: HttpMethod;
7
+ path: string;
8
+ openAPIObject: OpenAPIObject;
9
+ }) => string;
10
+ export declare function normalizeOpenAPIMixin({ mixinModule, log, cwd, }: {
11
+ mixinModule: NonNullable<NonNullable<NonNullable<NonNullable<VovkConfig['generatorConfig']>['segments']>[string]>['openAPIMixin']>;
12
+ log: ProjectInfo['log'];
13
+ cwd?: string;
14
+ }): Promise<NonNullable<NonNullable<NonNullable<VovkStrictConfig['generatorConfig']>['segments']>[string]>['openAPIMixin']>;
@@ -0,0 +1,114 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import * as YAML from 'yaml';
4
+ import chalkHighlightThing from './chalkHighlightThing.mjs';
5
+ import camelCase from 'lodash/camelCase.js';
6
+ import { generateFnName } from './generateFnName.mjs';
7
+ const getNamesNestJS = (operationObject) => {
8
+ const operationId = operationObject.operationId;
9
+ if (!operationId) {
10
+ throw new Error('Operation ID is required for NestJS module name generation');
11
+ }
12
+ const controllerHandlerMatch = operationId?.match(/^([A-Z][a-zA-Z0-9]*)_([a-zA-Z0-9_]+)/);
13
+ if (!controllerHandlerMatch) {
14
+ throw new Error(`Invalid operationId format for NestJS: ${operationId}`);
15
+ }
16
+ const [controllerName, handlerName] = controllerHandlerMatch.slice(1, 3);
17
+ return [controllerName.replace(/Controller$/, 'RPC'), handlerName];
18
+ };
19
+ const normalizeGetModuleName = (getModuleName) => {
20
+ if (getModuleName === 'nestjs-operation-id') {
21
+ getModuleName = ({ operationObject }) => getNamesNestJS(operationObject)[0];
22
+ }
23
+ else if (typeof getModuleName === 'string') {
24
+ const moduleName = getModuleName;
25
+ getModuleName = () => moduleName;
26
+ }
27
+ else if (typeof getModuleName !== 'function') {
28
+ throw new Error('getModuleName must be a function or one of the predefined strings');
29
+ }
30
+ return getModuleName;
31
+ };
32
+ const normalizeGetMethodName = (getMethodName) => {
33
+ if (getMethodName === 'nestjs-operation-id') {
34
+ getMethodName = ({ operationObject }) => getNamesNestJS(operationObject)[1];
35
+ }
36
+ else if (getMethodName === 'camel-case-operation-id') {
37
+ getMethodName = ({ operationObject }) => {
38
+ const operationId = operationObject.operationId;
39
+ if (!operationId) {
40
+ throw new Error('Operation ID is required for camel-case method name generation');
41
+ }
42
+ return camelCase(operationId);
43
+ };
44
+ }
45
+ else if (getMethodName === 'auto') {
46
+ getMethodName = ({ operationObject, method, path }) => {
47
+ const operationId = operationObject.operationId;
48
+ const isCamelCase = operationId && /^[a-z][a-zA-Z0-9]*$/.test(operationId);
49
+ const isSnakeCase = operationId && /^[a-z][a-z0-9_]+$/.test(operationId);
50
+ return isCamelCase ? operationId : isSnakeCase ? camelCase(operationId) : generateFnName(method, path);
51
+ };
52
+ }
53
+ else if (typeof getMethodName !== 'function') {
54
+ throw new Error('getMethodName must be a function or one of the predefined strings');
55
+ }
56
+ return getMethodName;
57
+ };
58
+ async function getOpenApiSpecLocal(openApiSpecFilePath, cwd) {
59
+ const openApiSpecAbsolutePath = path.resolve(cwd, openApiSpecFilePath);
60
+ const fileName = path.basename(openApiSpecAbsolutePath);
61
+ if (!fileName.endsWith('.json') && !fileName.endsWith('.yaml') && !fileName.endsWith('.yml')) {
62
+ throw new Error(`Invalid OpenAPI spec file format: ${fileName}. Please provide a JSON or YAML file.`);
63
+ }
64
+ const openApiSpecContent = await fs.readFile(openApiSpecAbsolutePath, 'utf8');
65
+ return (fileName.endsWith('.json') ? JSON.parse(openApiSpecContent) : YAML.parse(openApiSpecContent));
66
+ }
67
+ async function getOpenApiSpecRemote({ cwd, url, fallback, log, }) {
68
+ const resp = await fetch(url);
69
+ const text = await resp.text();
70
+ if (!resp.ok) {
71
+ if (fallback) {
72
+ log.warn(`Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${resp.status} ${resp.statusText}. Falling back to ${chalkHighlightThing(fallback)}`);
73
+ return getOpenApiSpecLocal(fallback, cwd);
74
+ }
75
+ throw new Error(`Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${resp.status} ${resp.statusText}`);
76
+ }
77
+ if (fallback) {
78
+ const fallbackAbsolutePath = path.resolve(cwd, fallback);
79
+ const existingFallback = await fs.readFile(fallbackAbsolutePath, 'utf8').catch(() => null);
80
+ if (existingFallback !== text) {
81
+ await fs.mkdir(path.dirname(fallbackAbsolutePath), { recursive: true });
82
+ await fs.writeFile(fallbackAbsolutePath, text);
83
+ log.info(`Saved OpenAPI spec to fallback file ${chalkHighlightThing(fallbackAbsolutePath)}`);
84
+ }
85
+ else {
86
+ log.debug(`OpenAPI spec at ${chalkHighlightThing(url)} is unchanged. Skipping write to fallback file ${chalkHighlightThing(fallbackAbsolutePath)}`);
87
+ }
88
+ }
89
+ return (text.trim().startsWith('{') || text.trim().startsWith('[') ? JSON.parse(text) : YAML.parse(text));
90
+ }
91
+ export async function normalizeOpenAPIMixin({
92
+ // mixinName,
93
+ mixinModule, log, cwd = process.cwd(), }) {
94
+ const { source, getModuleName, getMethodName } = mixinModule;
95
+ let openAPIObject;
96
+ if ('url' in source) {
97
+ openAPIObject = await getOpenApiSpecRemote({ url: source.url, fallback: source.fallback, log, cwd });
98
+ }
99
+ else if ('file' in source) {
100
+ openAPIObject = await getOpenApiSpecLocal(source.file, cwd);
101
+ }
102
+ else if ('object' in source) {
103
+ openAPIObject = source.object;
104
+ }
105
+ else {
106
+ throw new Error('Invalid source type for OpenAPI configuration');
107
+ }
108
+ return {
109
+ ...mixinModule,
110
+ source: { object: openAPIObject },
111
+ getModuleName: normalizeGetModuleName(getModuleName),
112
+ getMethodName: normalizeGetMethodName(getMethodName),
113
+ };
114
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.334",
3
+ "version": "0.0.1-draft.336",
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.396"
38
+ "vovk": "^3.0.0-draft.398"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.31"
@@ -1,7 +0,0 @@
1
- import type { PackageJson } from 'type-fest';
2
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
- export default function mergePackages({ rootPackageJson, packages, }: {
4
- rootPackageJson: PackageJson;
5
- packages: (PackageJson | undefined)[];
6
- log: ProjectInfo['log'];
7
- }): Promise<PackageJson>;
@@ -1,55 +0,0 @@
1
- import pick from 'lodash/pick.js';
2
- function mergeTwoPackageJsons(base, additional) {
3
- const merged = { ...base, ...additional };
4
- // TODO: Add deep merge for all properties
5
- if (base.scripts || additional.scripts) {
6
- merged.scripts = { ...base.scripts, ...additional.scripts };
7
- }
8
- if (base.dependencies || additional.dependencies) {
9
- merged.dependencies = { ...base.dependencies, ...additional.dependencies };
10
- }
11
- if (base.devDependencies || additional.devDependencies) {
12
- merged.devDependencies = { ...base.devDependencies, ...additional.devDependencies };
13
- }
14
- if (base.peerDependencies || additional.peerDependencies) {
15
- merged.peerDependencies = { ...base.peerDependencies, ...additional.peerDependencies };
16
- }
17
- return merged;
18
- }
19
- export default async function mergePackages({ rootPackageJson, packages, }) {
20
- const defaultPackageJson = {
21
- main: './index.cjs',
22
- module: './index.mjs',
23
- types: './index.d.mts',
24
- exports: {
25
- '.': {
26
- import: './index.mjs',
27
- require: './index.cjs',
28
- },
29
- './schema': {
30
- import: './schema.cjs',
31
- require: './schema.cjs',
32
- types: './schema.d.cts',
33
- },
34
- },
35
- };
36
- const pickedPackageJson = pick(rootPackageJson, [
37
- 'name',
38
- 'version',
39
- 'description',
40
- 'author',
41
- 'contributors',
42
- 'license',
43
- 'repository',
44
- 'homepage',
45
- 'bugs',
46
- 'keywords',
47
- ]);
48
- let result = { ...pickedPackageJson, ...defaultPackageJson };
49
- for (const pkg of packages) {
50
- if (pkg) {
51
- result = mergeTwoPackageJsons(result, pkg);
52
- }
53
- }
54
- return result;
55
- }
@@ -1,7 +0,0 @@
1
- import { VovkStrictConfig, type VovkConfig } from 'vovk';
2
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
- export declare function normalizeOpenAPIMixins({ mixinModules, log, cwd, }: {
4
- mixinModules: NonNullable<VovkConfig['openApiMixins']>;
5
- log: ProjectInfo['log'];
6
- cwd?: string;
7
- }): Promise<VovkStrictConfig['openApiMixins']>;
@@ -1,67 +0,0 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import * as YAML from 'yaml';
4
- import chalkHighlightThing from './chalkHighlightThing.mjs';
5
- async function getOpenApiSpecLocal(openApiSpecFilePath, cwd) {
6
- const openApiSpecAbsolutePath = path.resolve(cwd, openApiSpecFilePath);
7
- const fileName = path.basename(openApiSpecAbsolutePath);
8
- if (!fileName.endsWith('.json') && !fileName.endsWith('.yaml') && !fileName.endsWith('.yml')) {
9
- throw new Error(`Invalid OpenAPI spec file format: ${fileName}. Please provide a JSON or YAML file.`);
10
- }
11
- const openApiSpecContent = await fs.readFile(openApiSpecAbsolutePath, 'utf8');
12
- return (fileName.endsWith('.json') ? JSON.parse(openApiSpecContent) : YAML.parse(openApiSpecContent));
13
- }
14
- async function getOpenApiSpecRemote({ cwd, url, fallback, log, }) {
15
- const resp = await fetch(url);
16
- const text = await resp.text();
17
- if (!resp.ok) {
18
- if (fallback) {
19
- log.warn(`Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${resp.status} ${resp.statusText}. Falling back to ${chalkHighlightThing(fallback)}`);
20
- return getOpenApiSpecLocal(fallback, cwd);
21
- }
22
- throw new Error(`Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${resp.status} ${resp.statusText}`);
23
- }
24
- if (fallback) {
25
- const fallbackAbsolutePath = path.resolve(cwd, fallback);
26
- const existingFallback = await fs.readFile(fallbackAbsolutePath, 'utf8').catch(() => null);
27
- if (existingFallback !== text) {
28
- await fs.mkdir(path.dirname(fallbackAbsolutePath), { recursive: true });
29
- await fs.writeFile(fallbackAbsolutePath, text);
30
- log.info(`Saved OpenAPI spec to fallback file ${chalkHighlightThing(fallbackAbsolutePath)}`);
31
- }
32
- else {
33
- log.debug(`OpenAPI spec at ${chalkHighlightThing(url)} is unchanged. Skipping write to fallback file ${chalkHighlightThing(fallbackAbsolutePath)}`);
34
- }
35
- }
36
- return (text.trim().startsWith('{') || text.trim().startsWith('[') ? JSON.parse(text) : YAML.parse(text));
37
- }
38
- export async function normalizeOpenAPIMixins({ mixinModules, log, cwd = process.cwd(), }) {
39
- if (mixinModules) {
40
- const modules = await Promise.all(Object.entries(mixinModules).map(async ([mixinName, { source, apiRoot, getModuleName, getMethodName, errorMessageKey, package: packageJson },]) => {
41
- let openAPIObject;
42
- if ('url' in source) {
43
- openAPIObject = await getOpenApiSpecRemote({ url: source.url, fallback: source.fallback, log, cwd });
44
- }
45
- else if ('file' in source) {
46
- openAPIObject = await getOpenApiSpecLocal(source.file, cwd);
47
- }
48
- else if ('object' in source) {
49
- openAPIObject = source.object;
50
- }
51
- else {
52
- throw new Error('Invalid source type for OpenAPI configuration');
53
- }
54
- return {
55
- source: { object: openAPIObject },
56
- apiRoot,
57
- getModuleName,
58
- getMethodName,
59
- errorMessageKey,
60
- package: packageJson,
61
- mixinName,
62
- };
63
- }));
64
- return Object.fromEntries(modules.map((module) => [module.mixinName, module]));
65
- }
66
- return {};
67
- }