vovk-cli 0.0.1-draft.277 → 0.0.1-draft.279

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.
@@ -7,7 +7,7 @@ const { validateOnClient = null } = <%- t.imports.validateOnClient ? `require('$
7
7
  Object.keys(segment.controllers).forEach((rpcModuleName) => { %>
8
8
  exports.<%= rpcModuleName %> = createRPC(
9
9
  schema, '<%= segment.segmentName %>', '<%= rpcModuleName %>', fetcher,
10
- { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${segment.segmentApiRoot ?? t.apiRoot}'` %> }
10
+ { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${t.segmentMeta[segment.segmentName].forceApiRoot ?? t.apiRoot}'` %> }
11
11
  );
12
12
  <% })
13
13
  }) %>
@@ -11,7 +11,7 @@ Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach
11
11
  Object.keys(segment.controllers).forEach((rpcModuleName) => { %>
12
12
  export const <%= rpcModuleName %> = createRPC(
13
13
  schema, '<%= segment.segmentName %>', '<%= rpcModuleName %>', fetcher,
14
- { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${segment.segmentApiRoot ?? t.apiRoot}'` %> }
14
+ { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${t.segmentMeta[segment.segmentName].forceApiRoot ?? t.apiRoot}'` %> }
15
15
  );
16
16
  <%
17
17
  });
@@ -1,5 +1,6 @@
1
- # <%= t.package.name %> v<%= t.package.version %> ![TypeScript](https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white)
1
+ <%= t.readme.banner %>
2
2
 
3
+ # <%= t.package.name %> v<%= t.package.version %> ![TypeScript](https://badgen.net/badge/-/TypeScript?icon=typescript&label&labelColor=blue&color=555555)
3
4
 
4
5
  <%- t.package.description ? `> ${t.package.description}` : '' %>
5
6
 
@@ -20,7 +20,7 @@ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
20
20
  Object.keys(segment.controllers).forEach((rpcModuleName) => { %>
21
21
  export const <%= rpcModuleName %> = createRPC<<%= segment.segmentType === 'mixin' ? `MixinControllers` : `Controllers${i}` %>["<%= rpcModuleName %>"], Options>(
22
22
  schema, '<%= segment.segmentName %>', '<%= rpcModuleName %>', fetcher,
23
- { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${segment.segmentApiRoot ?? t.apiRoot}'` %> }
23
+ { validateOnClient, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : `apiRoot: '${t.segmentMeta[segment.segmentName].forceApiRoot ?? t.apiRoot}'` %> }
24
24
  );
25
25
  <% })
26
26
  }) %>
@@ -27,6 +27,8 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
27
27
  forceNothingWrittenLog: true,
28
28
  fullSchema,
29
29
  locatedSegments,
30
+ package: bundleConfig.package,
31
+ readme: bundleConfig.readme,
30
32
  cliGenerateOptions: {
31
33
  openapiSpec: cliBundleOptions?.openapiSpec,
32
34
  openapiGetModuleName: cliBundleOptions?.openapiGetModuleName,
@@ -1,12 +1,15 @@
1
- import { type VovkSchema } from 'vovk';
1
+ import { type VovkSchema, type VovkStrictConfig } from 'vovk';
2
+ import type { PackageJson } from 'type-fest';
2
3
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
4
  import type { GenerateOptions } from '../types.mjs';
4
5
  import type { Segment } from '../locateSegments.mjs';
5
- export declare function generate({ isEnsuringClient, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, }: {
6
+ export declare function generate({ isEnsuringClient, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, package: argPackageJson, readme: argReadme, }: {
6
7
  isEnsuringClient?: boolean;
7
8
  projectInfo: ProjectInfo;
8
9
  forceNothingWrittenLog?: boolean;
9
10
  fullSchema: VovkSchema;
10
11
  locatedSegments: Segment[];
11
12
  cliGenerateOptions?: GenerateOptions;
13
+ package?: PackageJson;
14
+ readme?: VovkStrictConfig['bundle']['readme'];
12
15
  }): Promise<void>;
@@ -13,6 +13,7 @@ import writeOneClientFile from './writeOneClientFile.mjs';
13
13
  import { ROOT_SEGMENT_FILE_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
14
14
  import { getTsconfig } from 'get-tsconfig';
15
15
  import { normalizeOpenAPIMixins } from '../utils/normalizeOpenAPIMixins.mjs';
16
+ import { BuiltInTemplateName } from '../getProjectInfo/getConfig/getTemplateDefs.mjs';
16
17
  const getIncludedSegmentNames = (config, fullSchema, configKey, cliGenerateOptions) => {
17
18
  const segments = Object.values(fullSchema.segments);
18
19
  const includeSegments = cliGenerateOptions?.[configKey === 'segmentedClient' ? 'segmentedIncludeSegments' : 'composedIncludeSegments'] ??
@@ -41,18 +42,26 @@ function logClientGenerationResults({ results, log, isEnsuringClient = false, fo
41
42
  const writtenResults = results.filter(({ written }) => written);
42
43
  const duration = Date.now() - startTime;
43
44
  const groupedByDir = _.groupBy(writtenResults, ({ outAbsoluteDir }) => outAbsoluteDir);
45
+ const logOrDebug = forceNothingWrittenLog ? log.info : log.debug;
44
46
  if (writtenResults.length) {
45
47
  for (const [outAbsoluteDir, dirResults] of Object.entries(groupedByDir)) {
46
48
  const templateNames = _.uniq(dirResults.map(({ templateName }) => templateName));
47
49
  log.info(`${clientType} client${isEnsuringClient ? ' placeholder' : ''} is generated to ${chalkHighlightThing(outAbsoluteDir)} from template${templateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(templateNames.map((s) => `"${s}"`).join(', '))} in ${duration}ms`);
48
50
  }
49
51
  }
50
- else if (!isEnsuringClient && fromTemplates.length) {
51
- const logOrDebug = forceNothingWrittenLog ? log.info : log.debug;
52
- for (const [outAbsoluteDir, dirResults] of Object.entries(groupedByDir)) {
53
- const templateNames = _.uniq(dirResults.map(({ templateName }) => templateName));
54
- logOrDebug(`${clientType} client that was generated to ${chalkHighlightThing(outAbsoluteDir)} from template${templateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(templateNames.map((s) => `"${s}"`).join(', '))} is up to date and doesn't need to be regenerated (${duration}ms)`);
52
+ else if (fromTemplates.length) {
53
+ if (!writtenResults.length) {
54
+ logOrDebug(`${clientType} client${isEnsuringClient ? ' placeholder' : ''} is up to date (${duration}ms)`);
55
55
  }
56
+ else if (!isEnsuringClient) {
57
+ for (const [outAbsoluteDir, dirResults] of Object.entries(groupedByDir)) {
58
+ const templateNames = _.uniq(dirResults.map(({ templateName }) => templateName));
59
+ logOrDebug(`${clientType} client that was generated to ${chalkHighlightThing(outAbsoluteDir)} from template${templateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(templateNames.map((s) => `"${s}"`).join(', '))} is up to date and doesn't need to be regenerated (${duration}ms)`);
60
+ }
61
+ }
62
+ }
63
+ else {
64
+ logOrDebug(`${clientType} client${isEnsuringClient ? ' placeholder' : ''} is not generated because no files were written (${duration}ms)`);
56
65
  }
57
66
  }
58
67
  const cliOptionsToOpenAPIMixins = ({ openapiGetMethodName, openapiGetModuleName, openapiRootUrl, openapiSpec, openapiMixinName, }) => {
@@ -62,7 +71,7 @@ const cliOptionsToOpenAPIMixins = ({ openapiGetMethodName, openapiGetModuleName,
62
71
  apiRoot: openapiRootUrl?.[i] ?? '/',
63
72
  getModuleName: openapiGetModuleName?.[i] ?? undefined,
64
73
  getMethodName: openapiGetMethodName?.[i] ?? 'auto',
65
- mixinName: openapiMixinName?.[i] ?? `mixin${i + 1}`, // TODO merge mixins with the same name
74
+ mixinName: openapiMixinName?.[i],
66
75
  };
67
76
  }) || []).map(({ source, apiRoot, getModuleName, getMethodName, mixinName }) => [
68
77
  mixinName,
@@ -75,25 +84,22 @@ const cliOptionsToOpenAPIMixins = ({ openapiGetMethodName, openapiGetModuleName,
75
84
  },
76
85
  ]));
77
86
  };
78
- export async function generate({ isEnsuringClient = false, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, }) {
87
+ export async function generate({ isEnsuringClient = false, projectInfo, forceNothingWrittenLog, fullSchema, locatedSegments, cliGenerateOptions, package: argPackageJson, readme: argReadme, }) {
79
88
  fullSchema = {
80
89
  ...fullSchema,
81
90
  // sort segments by name to avoid unnecessary rendering
82
91
  segments: Object.fromEntries(Object.entries(fullSchema.segments).sort(([a], [b]) => a.localeCompare(b))),
83
92
  };
84
- const { config, cwd, log, srcRoot } = projectInfo;
93
+ const { config, cwd, log, srcRoot, packageJson: rootPackageJson } = projectInfo;
85
94
  const allOpenAPIMixins = {
86
95
  ...config.openApiMixins,
87
96
  ...cliOptionsToOpenAPIMixins(cliGenerateOptions ?? {}),
88
97
  };
89
- /** @deprecated */
90
- let hasMixins = false;
91
98
  if (Object.keys(allOpenAPIMixins).length) {
92
- const mixins = Object.fromEntries(Object.entries(await normalizeOpenAPIMixins({ mixinModules: allOpenAPIMixins })).map(([mixinName, conf]) => [
99
+ const mixins = Object.fromEntries(Object.entries(await normalizeOpenAPIMixins({ mixinModules: allOpenAPIMixins, log })).map(([mixinName, conf]) => [
93
100
  mixinName,
94
101
  openAPIToVovkSchema({ ...conf, mixinName }).segments[mixinName],
95
102
  ]));
96
- hasMixins = true;
97
103
  fullSchema = {
98
104
  ...fullSchema,
99
105
  segments: {
@@ -119,7 +125,6 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
119
125
  config,
120
126
  cwd,
121
127
  log,
122
- hasMixins,
123
128
  cliGenerateOptions,
124
129
  configKey: 'composedClient',
125
130
  });
@@ -136,21 +141,27 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
136
141
  });
137
142
  const packageJson = await mergePackages({
138
143
  log,
139
- cwd,
140
- config,
141
- packages: [config.composedClient.package, templateDef.composedClient?.package],
144
+ rootPackageJson,
145
+ packages: [config.composedClient.package, templateDef.composedClient?.package, argPackageJson],
142
146
  });
147
+ const readme = Object.assign({}, config.composedClient.readme, templateDef.composedClient?.readme, argReadme);
148
+ const composedFullSchema = pickSegmentFullSchema(fullSchema, segmentNames);
149
+ const hasMixins = Object.values(composedFullSchema.segments).some((segment) => segment.segmentType === 'mixin');
150
+ if (templateName === BuiltInTemplateName.mixins && !hasMixins) {
151
+ return null;
152
+ }
143
153
  const { written } = await writeOneClientFile({
144
154
  cwd,
145
155
  projectInfo,
146
156
  clientTemplateFile,
147
- fullSchema: pickSegmentFullSchema(fullSchema, segmentNames),
157
+ fullSchema: composedFullSchema,
148
158
  prettifyClient: config.prettifyClient,
149
159
  segmentName: null,
150
160
  imports: clientImports.composedClient,
151
161
  templateContent,
152
162
  matterResult,
153
163
  package: packageJson,
164
+ readme,
154
165
  isEnsuringClient,
155
166
  outCwdRelativeDir,
156
167
  origin: config.origin ?? templateDef?.origin ?? null,
@@ -169,7 +180,7 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
169
180
  }));
170
181
  if (composedClientTemplateFiles.length) {
171
182
  logClientGenerationResults({
172
- results: composedClientResults,
183
+ results: composedClientResults.filter((result) => !!result),
173
184
  log,
174
185
  isEnsuringClient,
175
186
  forceNothingWrittenLog,
@@ -189,7 +200,6 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
189
200
  config,
190
201
  cwd,
191
202
  log,
192
- hasMixins,
193
203
  cliGenerateOptions,
194
204
  configKey: 'segmentedClient',
195
205
  });
@@ -206,25 +216,32 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
206
216
  outCwdRelativeDir,
207
217
  });
208
218
  const packageJson = await mergePackages({
209
- cwd,
210
- config,
211
219
  log,
220
+ rootPackageJson,
212
221
  packages: [
213
222
  config.segmentedClient.packages?.[segmentName],
214
223
  templateDef.segmentedClient?.packages?.[segmentName],
224
+ argPackageJson,
215
225
  ],
216
226
  });
227
+ const readme = Object.assign({}, config.segmentedClient.readmes?.[segmentName], templateDef.segmentedClient?.readmes?.[segmentName], argReadme);
228
+ const segmentedFullSchema = pickSegmentFullSchema(fullSchema, [segmentName]);
229
+ const hasMixins = Object.values(segmentedFullSchema.segments).some((segment) => segment.segmentType === 'mixin');
230
+ if (templateName === BuiltInTemplateName.mixins && !hasMixins) {
231
+ return null;
232
+ }
217
233
  const { written } = await writeOneClientFile({
218
234
  cwd,
219
235
  projectInfo,
220
236
  clientTemplateFile,
221
- fullSchema: pickSegmentFullSchema(fullSchema, [segmentName]),
237
+ fullSchema: segmentedFullSchema,
222
238
  prettifyClient: config.prettifyClient,
223
239
  segmentName,
224
240
  imports: clientImports.segmentedClient[segmentName],
225
241
  templateContent,
226
242
  matterResult,
227
243
  package: packageJson,
244
+ readme,
228
245
  isEnsuringClient,
229
246
  outCwdRelativeDir,
230
247
  origin: config.origin ?? templateDef?.origin ?? null,
@@ -243,7 +260,7 @@ export async function generate({ isEnsuringClient = false, projectInfo, forceNot
243
260
  // Remove unlisted directories in the output directory
244
261
  await removeUnlistedDirectories(outAbsoluteDir, segmentNames.map((s) => s || ROOT_SEGMENT_FILE_NAME));
245
262
  return {
246
- written: results.some(({ written }) => written),
263
+ written: results.filter((result) => !!result).some(({ written }) => written),
247
264
  templateName,
248
265
  outAbsoluteDir,
249
266
  };
@@ -8,14 +8,12 @@ export interface ClientTemplateFile {
8
8
  outCwdRelativeDir: string;
9
9
  templateDef: VovkStrictConfig['clientTemplateDefs'][string];
10
10
  }
11
- export default function getClientTemplateFiles({ config, cwd, log, configKey, cliGenerateOptions, hasMixins, }: {
11
+ export default function getClientTemplateFiles({ config, cwd, log, configKey, cliGenerateOptions, }: {
12
12
  config: VovkStrictConfig;
13
13
  cwd: string;
14
14
  log: ProjectInfo['log'];
15
15
  configKey: 'composedClient' | 'segmentedClient';
16
16
  cliGenerateOptions?: GenerateOptions;
17
- /** @deprecated */
18
- hasMixins: boolean;
19
17
  }): Promise<{
20
18
  fromTemplates: string[];
21
19
  templateFiles: ClientTemplateFile[];
@@ -4,7 +4,7 @@ import resolveAbsoluteModulePath from '../utils/resolveAbsoluteModulePath.mjs';
4
4
  import getFileSystemEntryType, { FileSystemEntryType } from '../utils/getFileSystemEntryType.mjs';
5
5
  import getPublicModuleNameFromPath from '../utils/getPublicModuleNameFromPath.mjs';
6
6
  import { BuiltInTemplateName } from '../getProjectInfo/getConfig/getTemplateDefs.mjs';
7
- export default async function getClientTemplateFiles({ config, cwd, log, configKey, cliGenerateOptions, hasMixins, }) {
7
+ export default async function getClientTemplateFiles({ config, cwd, log, configKey, cliGenerateOptions, }) {
8
8
  const usedTemplateDefs = {};
9
9
  const fromTemplates = configKey === 'composedClient'
10
10
  ? cliGenerateOptions?.composedFrom || cliGenerateOptions?.segmentedFrom
@@ -14,13 +14,13 @@ export default async function getClientTemplateFiles({ config, cwd, log, configK
14
14
  ? (cliGenerateOptions?.segmentedFrom ?? [])
15
15
  : config.segmentedClient.fromTemplates;
16
16
  const cliOutDir = configKey === 'composedClient' ? cliGenerateOptions?.composedOut : cliGenerateOptions?.segmentedOut;
17
- const configOutDir = configKey === 'composedClient' ? config.composedClient.outDir : config.segmentedClient.outDir;
17
+ const configOutDir = config[configKey].outDir;
18
18
  for (const templateName of fromTemplates) {
19
19
  if (!(templateName in config.clientTemplateDefs)) {
20
20
  throw new Error(`Unknown template name: ${templateName}`);
21
21
  }
22
22
  let usedDef = config.clientTemplateDefs[templateName];
23
- if (usedDef.isTsClient && hasMixins) {
23
+ if (usedDef.isTsClient) {
24
24
  usedDef = {
25
25
  ...usedDef,
26
26
  requires: {
@@ -1,9 +1,7 @@
1
1
  import type { PackageJson } from 'type-fest';
2
- import type { VovkStrictConfig } from 'vovk';
3
2
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
4
- export default function mergePackages({ cwd, packages, log, }: {
5
- cwd: string;
6
- config: VovkStrictConfig;
7
- log: ProjectInfo['log'];
3
+ export default function mergePackages({ rootPackageJson, packages, }: {
4
+ rootPackageJson: PackageJson;
8
5
  packages: (PackageJson | undefined)[];
6
+ log: ProjectInfo['log'];
9
7
  }): Promise<PackageJson>;
@@ -1,26 +1,4 @@
1
1
  import pick from 'lodash/pick.js';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- let cachedPromise;
5
- function getPackageJson(cwd, log) {
6
- const pkgPath = path.join(cwd, 'package.json');
7
- // If we have a cached promise, return it
8
- if (cachedPromise) {
9
- return cachedPromise;
10
- }
11
- const promise = fs
12
- .readFile(pkgPath, 'utf8')
13
- .then((content) => JSON.parse(content))
14
- .catch(() => {
15
- cachedPromise = undefined;
16
- log.warn(`Failed to read package.json at ${pkgPath}. Using a fallback.`);
17
- return {
18
- name: 'unknown',
19
- };
20
- });
21
- cachedPromise = promise;
22
- return promise;
23
- }
24
2
  function mergeTwoPackageJsons(base, additional) {
25
3
  const merged = { ...base, ...additional };
26
4
  // TODO: Add deep merge for all properties
@@ -38,8 +16,7 @@ function mergeTwoPackageJsons(base, additional) {
38
16
  }
39
17
  return merged;
40
18
  }
41
- export default async function mergePackages({ cwd, packages, log, }) {
42
- const fullPackageJson = await getPackageJson(cwd, log);
19
+ export default async function mergePackages({ rootPackageJson, packages, }) {
43
20
  const defaultPackageJson = {
44
21
  main: './index.cjs',
45
22
  module: './index.mjs',
@@ -56,7 +33,7 @@ export default async function mergePackages({ cwd, packages, log, }) {
56
33
  },
57
34
  },
58
35
  };
59
- const pickedPackageJson = pick(fullPackageJson, [
36
+ const pickedPackageJson = pick(rootPackageJson, [
60
37
  'name',
61
38
  'version',
62
39
  'description',
@@ -4,7 +4,7 @@ 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
- export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, isEnsuringClient, outCwdRelativeDir, origin, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, }: {
7
+ export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, isEnsuringClient, outCwdRelativeDir, origin, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, }: {
8
8
  cwd: string;
9
9
  projectInfo: ProjectInfo;
10
10
  clientTemplateFile: ClientTemplateFile;
@@ -20,6 +20,7 @@ export default function writeOneClientFile({ cwd, projectInfo, clientTemplateFil
20
20
  content: string;
21
21
  };
22
22
  package: PackageJson;
23
+ readme: VovkStrictConfig['bundle']['readme'];
23
24
  isEnsuringClient: boolean;
24
25
  outCwdRelativeDir: string;
25
26
  origin: string | null;
@@ -8,7 +8,7 @@ import TOML from '@iarna/toml';
8
8
  import prettify from '../utils/prettify.mjs';
9
9
  import { ROOT_SEGMENT_FILE_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
10
10
  import { compileJSONSchemaToTypeScriptType } from '../utils/compileJSONSchemaToTypeScriptType.mjs';
11
- export default async function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, isEnsuringClient, outCwdRelativeDir, origin, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, }) {
11
+ export default async function writeOneClientFile({ cwd, projectInfo, clientTemplateFile, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, package: packageJson, readme, isEnsuringClient, outCwdRelativeDir, origin, templateDef, locatedSegments, isNodeNextResolution, hasMixins, isVovkProject, }) {
12
12
  const { config, apiRoot } = projectInfo;
13
13
  const { templateFilePath, relativeDir } = clientTemplateFile;
14
14
  const locatedSegmentsByName = _.keyBy(locatedSegments, 'segmentName');
@@ -23,6 +23,7 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
23
23
  hasMixins,
24
24
  isVovkProject,
25
25
  package: packageJson,
26
+ readme,
26
27
  ROOT_SEGMENT_FILE_NAME,
27
28
  apiRoot: origin ? `${origin}/${config.rootEntry}` : apiRoot,
28
29
  imports,
@@ -53,7 +54,7 @@ export default async function writeOneClientFile({ cwd, projectInfo, clientTempl
53
54
  return [
54
55
  sName,
55
56
  {
56
- segmentApiRoot: forceApiRoot ??
57
+ forceApiRoot: forceApiRoot ??
57
58
  (segmentConfigOrigin || segmentConfigRootEntry
58
59
  ? `${segmentConfigOrigin ?? origin}/${segmentConfigRootEntry ?? config.rootEntry}`
59
60
  : null),
@@ -22,6 +22,9 @@ export default function getConfig({ configPath, cwd }: {
22
22
  includeSegments?: never;
23
23
  })) & {
24
24
  package?: import("type-fest").PackageJson;
25
+ readme?: {
26
+ banner?: string;
27
+ };
25
28
  };
26
29
  segmentedClient?: ({
27
30
  enabled?: boolean;
@@ -35,6 +38,9 @@ export default function getConfig({ configPath, cwd }: {
35
38
  includeSegments?: never;
36
39
  })) & {
37
40
  packages?: Record<string, import("type-fest").PackageJson>;
41
+ readmes?: Record<string, {
42
+ banner?: string;
43
+ }>;
38
44
  };
39
45
  bundle?: {
40
46
  outDir?: string;
@@ -42,6 +48,10 @@ export default function getConfig({ configPath, cwd }: {
42
48
  tsClientOutDir?: string;
43
49
  dontDeleteTsClientOutDirAfter?: boolean;
44
50
  sourcemap?: boolean;
51
+ package?: import("type-fest").PackageJson;
52
+ readme?: {
53
+ banner?: string;
54
+ };
45
55
  } & ({
46
56
  excludeSegments?: never;
47
57
  includeSegments?: string[];
@@ -67,7 +77,10 @@ export default function getConfig({ configPath, cwd }: {
67
77
  controller?: string;
68
78
  [key: string]: string | undefined;
69
79
  };
70
- libs?: Record<string, import("vovk").KnownAny>;
80
+ libs?: {
81
+ ajv: import("vovk").KnownAny;
82
+ [key: string]: import("vovk").KnownAny;
83
+ };
71
84
  segmentConfig?: false | {
72
85
  [x: string]: {
73
86
  origin?: string;
@@ -81,10 +94,14 @@ export default function getConfig({ configPath, cwd }: {
81
94
  file: string;
82
95
  } | {
83
96
  url: string;
84
- cache?: string;
97
+ fallback?: string;
85
98
  } | {
86
99
  object: import("openapi3-ts/oas31").OpenAPIObject;
87
100
  };
101
+ package?: import("type-fest").PackageJson;
102
+ readme?: {
103
+ banner?: string;
104
+ };
88
105
  apiRoot?: string;
89
106
  getModuleName?: "nestjs-operation-id" | (string & {}) | "api" | import("vovk/mjs/types").GetOpenAPINameFn;
90
107
  getMethodName?: "nestjs-operation-id" | "camel-case-operation-id" | "auto" | import("vovk/mjs/types").GetOpenAPINameFn;
@@ -43,7 +43,6 @@ export default async function getConfig({ configPath, cwd }) {
43
43
  outDir: conf.segmentedClient?.outDir ?? path.join(srcRoot ?? '.', 'client'),
44
44
  },
45
45
  bundle: {
46
- ...conf.bundle,
47
46
  outDir: conf.bundle?.outDir ?? 'dist',
48
47
  tsClientOutDir: conf.bundle?.tsClientOutDir ?? 'tmp_ts_rpc',
49
48
  dontDeleteTsClientOutDirAfter: conf.bundle?.dontDeleteTsClientOutDirAfter ?? false,
@@ -52,6 +51,9 @@ export default async function getConfig({ configPath, cwd }) {
52
51
  [BuiltInTemplateName.readme]: '.',
53
52
  [BuiltInTemplateName.packageJson]: '.',
54
53
  },
54
+ package: {},
55
+ readme: {},
56
+ ...conf.bundle,
55
57
  },
56
58
  modulesDir: conf.modulesDir ?? path.join(srcRoot ?? '.', 'modules'),
57
59
  schemaOutDir: env.VOVK_SCHEMA_OUT_DIR ?? conf.schemaOutDir ?? './.vovk-schema',
@@ -68,10 +70,7 @@ export default async function getConfig({ configPath, cwd }) {
68
70
  },
69
71
  libs: conf.libs ?? {},
70
72
  segmentConfig: conf.segmentConfig ?? {},
71
- openApiMixins: await normalizeOpenAPIMixins({
72
- mixinModules: conf.openApiMixins ?? {},
73
- cwd,
74
- }),
73
+ openApiMixins: {},
75
74
  };
76
75
  if (typeof conf.emitConfig === 'undefined') {
77
76
  config.emitConfig = ['$schema', 'libs'];
@@ -83,6 +82,11 @@ export default async function getConfig({ configPath, cwd }) {
83
82
  config.emitConfig = conf.emitConfig;
84
83
  } // else it's false and emitConfig already is []
85
84
  const log = getLogger(config.logLevel);
85
+ config.openApiMixins = await normalizeOpenAPIMixins({
86
+ mixinModules: conf.openApiMixins ?? {},
87
+ cwd,
88
+ log,
89
+ });
86
90
  if (!userConfig) {
87
91
  log.warn(`Unable to load config at ${chalkHighlightThing(cwd)}. Using default values. ${error ?? ''}`);
88
92
  }
@@ -11,6 +11,7 @@ export default function getProjectInfo({ port: givenPort, cwd, configPath, srcRo
11
11
  apiDirAbsolutePath: string | null;
12
12
  srcRoot: string | null;
13
13
  config: import("vovk").VovkStrictConfig;
14
+ packageJson: import("type-fest").PackageJson;
14
15
  log: {
15
16
  info: (msg: string) => void;
16
17
  warn: (msg: string) => void;
@@ -1,5 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import getConfig from './getConfig/index.mjs';
3
+ import { getPackageJson } from '../utils/getPackageJson.mjs';
3
4
  export default async function getProjectInfo({ port: givenPort, cwd = process.cwd(), configPath, srcRootRequired = true, } = {}) {
4
5
  const port = givenPort?.toString() ?? process.env.PORT ?? '3000';
5
6
  // Make PORT available to the config file at getConfig
@@ -8,6 +9,7 @@ export default async function getProjectInfo({ port: givenPort, cwd = process.cw
8
9
  configPath,
9
10
  cwd,
10
11
  });
12
+ const packageJson = await getPackageJson(cwd, log);
11
13
  if (srcRootRequired && !srcRoot) {
12
14
  throw new Error(`Could not find app router directory at ${cwd}. Check Next.js docs for more info.`);
13
15
  }
@@ -23,6 +25,7 @@ export default async function getProjectInfo({ port: givenPort, cwd = process.cw
23
25
  apiDirAbsolutePath,
24
26
  srcRoot,
25
27
  config,
28
+ packageJson,
26
29
  log,
27
30
  };
28
31
  }
@@ -15,9 +15,9 @@ export default async function createConfig({ root, log, options: { validationLib
15
15
  controller: 'vovk-cli/module-templates/controller.ts.ejs',
16
16
  service: 'vovk-cli/module-templates/service.ts.ejs',
17
17
  };
18
+ config.imports ??= {};
19
+ config.imports.validateOnClient = validationLibrary === 'vovk-dto' ? 'vovk-dto/validateOnClient' : 'vovk-ajv';
18
20
  if (validationLibrary) {
19
- config.imports ??= {};
20
- config.imports.validateOnClient = 'vovk-ajv';
21
21
  try {
22
22
  const validationTemplates = await getTemplateFilesFromPackage(validationLibrary, channel);
23
23
  Object.assign(moduleTemplates, validationTemplates);
@@ -19,7 +19,7 @@ export class Init {
19
19
  log;
20
20
  async #init({ configPaths, pkgJson, }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, reactQuery, lang, dryRun, channel, }) {
21
21
  const { log, root } = this;
22
- const dependencies = ['vovk', 'vovk-client'];
22
+ const dependencies = ['vovk', 'vovk-client', 'vovk-ajv', 'openapi3-ts'];
23
23
  const devDependencies = ['vovk-cli'];
24
24
  if (lang?.includes('py')) {
25
25
  dependencies.push('vovk-python');
@@ -33,9 +33,8 @@ export class Init {
33
33
  log.debug(`Deleted existing config file${configPaths.length > 1 ? 's' : ''} at ${configPaths.join(', ')}`);
34
34
  }
35
35
  if (validationLibrary) {
36
- dependencies.push(validationLibrary, 'vovk-ajv', ...({
36
+ dependencies.push(validationLibrary, ...({
37
37
  'vovk-zod': ['zod'],
38
- 'vovk-yup': ['yup'],
39
38
  'vovk-dto': ['class-validator', 'class-transformer', 'dto-mapped-types', 'reflect-metadata'],
40
39
  }[validationLibrary] ?? []));
41
40
  }
@@ -185,11 +184,6 @@ export class Init {
185
184
  value: 'vovk-zod',
186
185
  description: 'Use Zod for data validation',
187
186
  },
188
- {
189
- name: 'vovk-yup',
190
- value: 'vovk-yup',
191
- description: 'Use Yup for data validation',
192
- },
193
187
  {
194
188
  name: 'vovk-dto',
195
189
  value: 'vovk-dto',
@@ -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-yup", "vovk-dto" or another); set to "none" to skip')
17
+ .option('--validation-library <library>', 'validation library to use ("vovk-zod", "vovk-dto" or another); set to "none" to skip')
18
18
  .option('--react-query', 'use @tanstack/react-query for data fetching inside components')
19
19
  .option('--channel <channel>', 'channel to use for fetching packages', 'latest')
20
20
  .option('--dry-run', 'do not write files to disk')
@@ -0,0 +1,3 @@
1
+ import { PackageJson } from 'type-fest';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ export declare function getPackageJson(cwd: string, log: ProjectInfo['log']): Promise<PackageJson>;
@@ -0,0 +1,22 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ let cachedPromise;
4
+ export function getPackageJson(cwd, log) {
5
+ const pkgPath = path.join(cwd, 'package.json');
6
+ // If we have a cached promise, return it
7
+ if (cachedPromise) {
8
+ return cachedPromise;
9
+ }
10
+ const promise = fs
11
+ .readFile(pkgPath, 'utf8')
12
+ .then((content) => JSON.parse(content))
13
+ .catch(() => {
14
+ cachedPromise = undefined;
15
+ log.warn(`Failed to read package.json at ${pkgPath}. Using a fallback.`);
16
+ return {
17
+ name: 'unknown',
18
+ };
19
+ });
20
+ cachedPromise = promise;
21
+ return promise;
22
+ }
@@ -1,5 +1,7 @@
1
1
  import { VovkStrictConfig, type VovkConfig } from 'vovk';
2
- export declare function normalizeOpenAPIMixins({ mixinModules, cwd, }: {
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ export declare function normalizeOpenAPIMixins({ mixinModules, log, cwd, }: {
3
4
  mixinModules: NonNullable<VovkConfig['openApiMixins']>;
5
+ log: ProjectInfo['log'];
4
6
  cwd?: string;
5
7
  }): Promise<VovkStrictConfig['openApiMixins']>;
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import * as YAML from 'yaml';
4
+ import chalkHighlightThing from './chalkHighlightThing.mjs';
4
5
  async function getOpenApiSpecLocal(openApiSpecFilePath, cwd) {
5
6
  const openApiSpecAbsolutePath = path.resolve(cwd, openApiSpecFilePath);
6
7
  const fileName = path.basename(openApiSpecAbsolutePath);
@@ -10,20 +11,36 @@ async function getOpenApiSpecLocal(openApiSpecFilePath, cwd) {
10
11
  const openApiSpecContent = await fs.readFile(openApiSpecAbsolutePath, 'utf8');
11
12
  return (fileName.endsWith('.json') ? JSON.parse(openApiSpecContent) : YAML.parse(openApiSpecContent));
12
13
  }
13
- async function getOpenApiSpecRemote(openApiSpecUrl) {
14
- const resp = await fetch(openApiSpecUrl);
14
+ async function getOpenApiSpecRemote({ cwd, url, fallback, log, }) {
15
+ const resp = await fetch(url);
15
16
  const text = await resp.text();
16
17
  if (!resp.ok) {
17
- throw new Error(`Failed to fetch OpenAPI spec from ${openApiSpecUrl}: ${resp.status} ${resp.statusText}`);
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
+ }
18
35
  }
19
36
  return (text.trim().startsWith('{') || text.trim().startsWith('[') ? JSON.parse(text) : YAML.parse(text));
20
37
  }
21
- export async function normalizeOpenAPIMixins({ mixinModules, cwd = process.cwd(), }) {
38
+ export async function normalizeOpenAPIMixins({ mixinModules, log, cwd = process.cwd(), }) {
22
39
  if (mixinModules) {
23
40
  const modules = await Promise.all(Object.entries(mixinModules).map(async ([mixinName, { source, apiRoot, getModuleName, getMethodName, errorMessageKey }]) => {
24
41
  let openAPIObject;
25
42
  if ('url' in source) {
26
- openAPIObject = await getOpenApiSpecRemote(source.url);
43
+ openAPIObject = await getOpenApiSpecRemote({ url: source.url, fallback: source.fallback, log, cwd });
27
44
  }
28
45
  else if ('file' in source) {
29
46
  openAPIObject = await getOpenApiSpecLocal(source.file, cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.277",
3
+ "version": "0.0.1-draft.279",
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.297"
38
+ "vovk": "^3.0.0-draft.315"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "vovk-python": "^0.0.1-draft.41"