vovk-cli 0.2.0 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/client-templates/schema-ts/schema.ts.ejs +1 -1
  3. package/client-templates/ts-base/index.ts.ejs +1 -1
  4. package/dist/bundle/index.mjs +64 -42
  5. package/dist/dev/diff-segment-schema.d.mts +0 -17
  6. package/dist/dev/diff-segment-schema.mjs +1 -17
  7. package/dist/dev/ensure-schema-files.mjs +28 -10
  8. package/dist/dev/index.mjs +19 -13
  9. package/dist/dev/log-diff-result.d.mts +1 -1
  10. package/dist/dev/log-diff-result.mjs +1 -1
  11. package/dist/dev/write-meta-json.mjs +2 -2
  12. package/dist/dev/write-one-segment-schema-file.mjs +7 -2
  13. package/dist/generate/ensure-client.mjs +1 -1
  14. package/dist/generate/generate.mjs +38 -25
  15. package/dist/generate/get-client-template-files.mjs +8 -4
  16. package/dist/generate/get-project-full-schema.d.mts +1 -1
  17. package/dist/generate/get-project-full-schema.mjs +2 -2
  18. package/dist/generate/index.d.mts +2 -2
  19. package/dist/generate/index.mjs +46 -34
  20. package/dist/generate/write-one-client-file.d.mts +3 -3
  21. package/dist/generate/write-one-client-file.mjs +8 -6
  22. package/dist/get-project-info/get-config/get-relative-src-root.mjs +1 -1
  23. package/dist/get-project-info/get-config/get-template-defs.d.mts +20 -23
  24. package/dist/get-project-info/get-config/get-template-defs.mjs +21 -47
  25. package/dist/get-project-info/get-config/get-user-config.mjs +1 -1
  26. package/dist/get-project-info/get-config/index.d.mts +1 -1
  27. package/dist/get-project-info/get-config/index.mjs +15 -10
  28. package/dist/get-project-info/get-meta-schema.d.mts +1 -1
  29. package/dist/get-project-info/get-meta-schema.mjs +1 -1
  30. package/dist/get-project-info/index.mjs +2 -2
  31. package/dist/index.mjs +25 -16
  32. package/dist/init/check-tsconfig-for-experimental-decorators.mjs +1 -1
  33. package/dist/init/create-config.d.mts +1 -1
  34. package/dist/init/create-config.mjs +5 -5
  35. package/dist/init/index.d.mts +1 -1
  36. package/dist/init/index.mjs +49 -17
  37. package/dist/init/install-dependencies.d.mts +2 -1
  38. package/dist/init/install-dependencies.mjs +10 -2
  39. package/dist/init/update-dependencies-without-installing.d.mts +1 -1
  40. package/dist/init/update-dependencies-without-installing.mjs +0 -1
  41. package/dist/init/update-gitignore.d.mts +2 -0
  42. package/dist/init/update-gitignore.mjs +20 -0
  43. package/dist/init/update-typescript-config.mjs +1 -1
  44. package/dist/new/index.d.mts +1 -1
  45. package/dist/new/new-module.mjs +11 -16
  46. package/dist/new/new-segment.mjs +9 -4
  47. package/dist/new/render.mjs +1 -1
  48. package/dist/types.d.mts +1 -1
  49. package/dist/utils/compile-json-schema-to-typescript-type.d.mts +1 -1
  50. package/dist/utils/compile-ts.mjs +1 -1
  51. package/dist/utils/generate-fn-name.d.mts +0 -9
  52. package/dist/utils/generate-fn-name.mjs +2 -9
  53. package/dist/utils/generated-banner.d.mts +1 -0
  54. package/dist/utils/generated-banner.mjs +3 -0
  55. package/dist/utils/get-available-port.d.mts +0 -8
  56. package/dist/utils/get-available-port.mjs +2 -14
  57. package/dist/utils/get-file-system-entry-type.d.mts +5 -4
  58. package/dist/utils/get-file-system-entry-type.mjs +5 -5
  59. package/dist/utils/normalize-openapi-mixin.d.mts +1 -1
  60. package/dist/utils/normalize-openapi-mixin.mjs +18 -3
  61. package/dist/utils/prettify.d.mts +4 -0
  62. package/dist/utils/prettify.mjs +39 -3
  63. package/dist/utils/remove-unlisted-directories.d.mts +1 -9
  64. package/dist/utils/remove-unlisted-directories.mjs +60 -21
  65. package/dist/utils/resolve-absolute-module-path.mjs +1 -2
  66. package/dist/utils/update-config-property.mjs +1 -1
  67. package/package.json +23 -30
  68. package/client-templates/js-base/index.d.ts.ejs +0 -21
  69. package/client-templates/js-base/index.js.ejs +0 -18
  70. package/client-templates/openapi-js/openapi.d.ts.ejs +0 -4
  71. package/client-templates/openapi-js/openapi.js.ejs +0 -4
  72. package/client-templates/schema-js/schema.d.ts.ejs +0 -10
  73. package/client-templates/schema-js/schema.js.ejs +0 -29
@@ -1,25 +1,30 @@
1
- import { confirm, select, checkbox } from '@inquirer/prompts';
2
- import path from 'node:path';
3
1
  import fs from 'node:fs/promises';
4
- import chalk from 'chalk';
2
+ import path from 'node:path';
3
+ import { checkbox, confirm, select } from '@inquirer/prompts';
5
4
  import NPMCliPackageJson from '@npmcli/package-json';
5
+ import chalk from 'chalk';
6
6
  import { getConfigAbsolutePaths } from '../get-project-info/get-config/get-config-absolute-paths.mjs';
7
+ import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
7
8
  import { getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
8
- import { installDependencies, getPackageManager } from './install-dependencies.mjs';
9
9
  import { getLogger } from '../utils/get-logger.mjs';
10
- import { createConfig } from './create-config.mjs';
11
- import { updateNPMScripts, getDevScript } from './update-npm-scripts.mjs';
12
10
  import { checkTSConfigForExperimentalDecorators } from './check-tsconfig-for-experimental-decorators.mjs';
13
- import { updateTypeScriptConfig } from './update-typescript-config.mjs';
14
- import { updateDependenciesWithoutInstalling } from './update-dependencies-without-installing.mjs';
11
+ import { createConfig } from './create-config.mjs';
12
+ import { getPackageManager, installDependencies } from './install-dependencies.mjs';
15
13
  import { logUpdateDependenciesError } from './log-update-dependencies-error.mjs';
16
- import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
14
+ import { updateDependenciesWithoutInstalling } from './update-dependencies-without-installing.mjs';
15
+ import { updateGitignore } from './update-gitignore.mjs';
16
+ import { getDevScript, updateNPMScripts } from './update-npm-scripts.mjs';
17
+ import { updateTypeScriptConfig } from './update-typescript-config.mjs';
18
+ const VALIDATION_LIBRARIES = ['zod', 'valibot', 'arktype', 'none'];
19
+ const LANGS = ['py', 'rs'];
20
+ const UPDATE_SCRIPTS_MODES = ['implicit', 'explicit'];
21
+ const CHANNELS = ['latest', 'beta', 'draft'];
17
22
  export class Init {
18
23
  root;
19
24
  log;
20
25
  async #init({ configPaths, pkgJson, }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, bundle, lang, dryRun, channel, }) {
21
26
  const { log, root } = this;
22
- const dependencies = ['vovk', 'vovk-client', 'vovk-ajv'];
27
+ const dependencies = ['vovk', 'vovk-ajv'];
23
28
  const devDependencies = ['vovk-cli'];
24
29
  if (lang?.includes('py')) {
25
30
  devDependencies.push('vovk-python');
@@ -29,8 +34,9 @@ export class Init {
29
34
  }
30
35
  // delete older config files
31
36
  if (configPaths.length) {
32
- await Promise.all(configPaths.map((configPath) => fs.rm(configPath)));
33
- log.debug(`Deleted existing config file${configPaths.length > 1 ? 's' : ''} at ${configPaths.join(', ')}`);
37
+ if (!dryRun)
38
+ await Promise.all(configPaths.map((configPath) => fs.rm(configPath)));
39
+ log.debug(`${dryRun ? 'Dry run: would delete' : 'Deleted'} existing config file${configPaths.length > 1 ? 's' : ''} at ${configPaths.join(', ')}`);
34
40
  }
35
41
  if (validationLibrary) {
36
42
  dependencies.push(...({
@@ -40,14 +46,14 @@ export class Init {
40
46
  }[validationLibrary] ?? []));
41
47
  }
42
48
  if (bundle) {
43
- const TSDOWN_VERSION = '0.19.0';
49
+ const TSDOWN_VERSION = '0.22.14';
44
50
  devDependencies.push(`tsdown@${TSDOWN_VERSION}`);
45
51
  }
46
52
  if (updateScripts) {
47
53
  try {
48
54
  if (!dryRun && pkgJson)
49
55
  await updateNPMScripts({ pkgJson, root, bundle, updateScriptsMode: updateScripts });
50
- log.info('Updated scripts at package.json');
56
+ log.info(`${dryRun ? 'Dry run: would update' : 'Updated'} scripts at package.json`);
51
57
  }
52
58
  catch (error) {
53
59
  log.error(`Failed to update scripts at package.json: ${error.message}`);
@@ -63,7 +69,7 @@ export class Init {
63
69
  };
64
70
  if (!dryRun)
65
71
  await updateTypeScriptConfig(root, compilerOptions);
66
- log.info(`Added ${Object.keys(compilerOptions)
72
+ log.info(`${dryRun ? 'Dry run: would add' : 'Added'} ${Object.keys(compilerOptions)
67
73
  .map((k) => `"${k}"`)
68
74
  .join(' and ')} to tsconfig.json`);
69
75
  }
@@ -73,7 +79,7 @@ export class Init {
73
79
  }
74
80
  if (!dryRun && pkgJson) {
75
81
  let depsUpdated = false;
76
- const packageManager = getPackageManager({ useNpm, useYarn, usePnpm, useBun, pkgJson });
82
+ const packageManager = getPackageManager({ useNpm, useYarn, usePnpm, useBun, pkgJson, log });
77
83
  try {
78
84
  await updateDependenciesWithoutInstalling({
79
85
  log,
@@ -114,13 +120,23 @@ export class Init {
114
120
  }
115
121
  }
116
122
  }
123
+ try {
124
+ if (!dryRun) {
125
+ const gitignoreEntry = await updateGitignore(root);
126
+ if (gitignoreEntry)
127
+ log.info(`Added ${chalkHighlightThing(gitignoreEntry)} to .gitignore`);
128
+ }
129
+ }
130
+ catch (error) {
131
+ log.error(`Failed to update .gitignore: ${error.message}`);
132
+ }
117
133
  try {
118
134
  const { configAbsolutePath } = await createConfig({
119
135
  root,
120
136
  log,
121
137
  options: { validationLibrary, channel, bundle, lang, dryRun },
122
138
  });
123
- log.info(`Config created successfully at ${chalkHighlightThing(configAbsolutePath)}`);
139
+ log.info(`Config ${dryRun ? 'would be created (dry run)' : 'created successfully'} at ${chalkHighlightThing(configAbsolutePath)}`);
124
140
  log.info(`You can now create a root segment with ${chalkHighlightThing('npx vovk new segment')} command`);
125
141
  }
126
142
  catch (error) {
@@ -131,6 +147,22 @@ export class Init {
131
147
  const cwd = process.cwd();
132
148
  const root = path.resolve(cwd, prefix ?? '.');
133
149
  const log = getLogger(logLevel ?? 'info');
150
+ validationLibrary = validationLibrary?.toLowerCase();
151
+ lang = lang?.map((l) => l.toLowerCase());
152
+ if (validationLibrary && !VALIDATION_LIBRARIES.includes(validationLibrary)) {
153
+ throw new Error(`Unknown --validation-library value "${validationLibrary}". Supported values: ${VALIDATION_LIBRARIES.join(', ')}`);
154
+ }
155
+ for (const l of lang ?? []) {
156
+ if (!LANGS.includes(l)) {
157
+ throw new Error(`Unknown --lang value "${l}". Supported values: ${LANGS.join(', ')}`);
158
+ }
159
+ }
160
+ if (updateScripts && !UPDATE_SCRIPTS_MODES.includes(updateScripts)) {
161
+ throw new Error(`Unknown --update-scripts value "${updateScripts}". Supported values: ${UPDATE_SCRIPTS_MODES.join(', ')}`);
162
+ }
163
+ if (channel && !CHANNELS.includes(channel)) {
164
+ throw new Error(`Unknown --channel value "${channel}". Supported values: ${CHANNELS.join(', ')}`);
165
+ }
134
166
  const pkgJson = await NPMCliPackageJson.load(root).catch(() => null);
135
167
  this.root = root;
136
168
  this.log = log;
@@ -1,9 +1,10 @@
1
1
  import type NPMCliPackageJson from '@npmcli/package-json';
2
- import type { getLogger } from '../utils/get-logger.mjs';
3
2
  import type { InitOptions } from '../types.mjs';
3
+ import type { getLogger } from '../utils/get-logger.mjs';
4
4
  export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun';
5
5
  export declare function getPackageManager(options: Pick<InitOptions, 'useNpm' | 'useYarn' | 'usePnpm' | 'useBun'> & {
6
6
  pkgJson: NPMCliPackageJson;
7
+ log?: ReturnType<typeof getLogger>;
7
8
  }): PackageManager;
8
9
  export declare function installDependencies({ log, cwd, packageManager, }: {
9
10
  log: ReturnType<typeof getLogger>;
@@ -1,5 +1,6 @@
1
1
  import { spawn } from 'node:child_process';
2
2
  import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
3
+ const KNOWN_PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm', 'bun'];
3
4
  export function getPackageManager(options) {
4
5
  if (options.useNpm)
5
6
  return 'npm';
@@ -9,8 +10,15 @@ export function getPackageManager(options) {
9
10
  return 'pnpm';
10
11
  if (options.useBun)
11
12
  return 'bun';
12
- const packageManager = options.pkgJson.content?.packageManager;
13
- return packageManager ? packageManager.split('@')[0] : 'npm'; // Default to npm if no options are true
13
+ const packageManager = options.pkgJson.content?.packageManager?.split('@')[0];
14
+ if (!packageManager)
15
+ return 'npm'; // Default to npm if no options are true
16
+ // this name gets spawned, so an unknown one from package.json is not executed
17
+ if (!KNOWN_PACKAGE_MANAGERS.includes(packageManager)) {
18
+ options.log?.warn(`Unknown "packageManager" ${JSON.stringify(packageManager)} in package.json, using ${chalkHighlightThing('npm')} instead`);
19
+ return 'npm';
20
+ }
21
+ return packageManager;
14
22
  }
15
23
  export async function installDependencies({ log, cwd, packageManager, }) {
16
24
  log.info(`Installing dependencies at ${chalkHighlightThing(cwd)} using ${chalkHighlightThing(packageManager)}...`);
@@ -1,5 +1,5 @@
1
- import type { getLogger } from '../utils/get-logger.mjs';
2
1
  import type { InitOptions } from '../types.mjs';
2
+ import type { getLogger } from '../utils/get-logger.mjs';
3
3
  export declare function updateDependenciesWithoutInstalling({ log, dir, dependencyNames, devDependencyNames, channel, }: {
4
4
  log: ReturnType<typeof getLogger>;
5
5
  dir: string;
@@ -8,7 +8,6 @@ const packagesRoot = path.resolve(import.meta.dirname, '../../..');
8
8
  const localPackageDirs = {
9
9
  vovk: 'vovk',
10
10
  'vovk-cli': 'vovk-cli',
11
- 'vovk-client': 'vovk-client',
12
11
  'vovk-ajv': 'vovk-ajv',
13
12
  'vovk-python': 'vovk-python',
14
13
  'vovk-rust': 'vovk-rust',
@@ -0,0 +1,2 @@
1
+ /** Adds the default composed client outDir to .gitignore. Returns the added entry or null if already present. */
2
+ export declare function updateGitignore(root: string): Promise<string | null>;
@@ -0,0 +1,20 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getRelativeSrcRoot } from '../get-project-info/get-config/get-relative-src-root.mjs';
4
+ /** Adds the default composed client outDir to .gitignore. Returns the added entry or null if already present. */
5
+ export async function updateGitignore(root) {
6
+ const srcRoot = await getRelativeSrcRoot({ cwd: root });
7
+ const outDir = path.join(srcRoot ?? '.', 'client');
8
+ const entry = `/${outDir}`;
9
+ const gitignorePath = path.join(root, '.gitignore');
10
+ const content = await fs.readFile(gitignorePath, 'utf-8').catch(() => '');
11
+ const hasEntry = content.split('\n').some((line) => {
12
+ const trimmed = line.trim().replace(/\/$/, '');
13
+ return trimmed === entry || trimmed === outDir;
14
+ });
15
+ if (hasEntry)
16
+ return null;
17
+ const updated = `${content.length && !content.endsWith('\n') ? `${content}\n` : content}\n# vovk generated client\n${entry}\n`;
18
+ await fs.writeFile(gitignorePath, updated, 'utf-8');
19
+ return entry;
20
+ }
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  import * as jsonc from 'jsonc-parser';
4
4
  import { prettify } from '../utils/prettify.mjs';
5
5
  export async function updateTypeScriptConfig(root, compilerOptions) {
@@ -1,3 +1,3 @@
1
- import type { NewOptions } from '../types.mjs';
2
1
  import type { ProjectInfo } from '../get-project-info/index.mts';
2
+ import type { NewOptions } from '../types.mjs';
3
3
  export declare function newComponents(components: string[], projectInfo: ProjectInfo, { dryRun, outDir, templates, overwrite, noSegmentUpdate, empty, static: isStaticSegment }: NewOptions): Promise<void>;
@@ -1,16 +1,15 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  import chalk from 'chalk';
4
- import _ from 'lodash';
5
4
  import { getTsconfig } from 'get-tsconfig';
6
- import { render } from './render.mjs';
7
- import { addClassToSegmentCode } from './add-class-to-segment-code.mjs';
8
5
  import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
9
6
  import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
10
7
  import { getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
8
+ import { locateSegments } from '../utils/locate-segments.mjs';
11
9
  import { prettify } from '../utils/prettify.mjs';
12
10
  import { resolveAbsoluteModulePath } from '../utils/resolve-absolute-module-path.mjs';
13
- import { locateSegments } from '../utils/locate-segments.mjs';
11
+ import { addClassToSegmentCode } from './add-class-to-segment-code.mjs';
12
+ import { render } from './render.mjs';
14
13
  function splitByLast(str, delimiter = '/') {
15
14
  const index = str.lastIndexOf(delimiter);
16
15
  if (index === -1) {
@@ -82,16 +81,6 @@ export async function newModule({ projectInfo, what, moduleNameWithOptionalSegme
82
81
  }
83
82
  const absoluteModuleDir = path.join(cwd, outDir);
84
83
  const absoluteModulePath = path.join(absoluteModuleDir, fileName);
85
- // Pre-kebab-case projects keep modules at e.g. userProfile/UserProfileController.ts; scaffolding
86
- // user-profile/user-profile-controller.ts next to it would register a duplicate class in the segment.
87
- const legacySuffix = type === 'controller' ? 'Controller' : type === 'service' ? 'Service' : null;
88
- if (!outDirFlag && legacySuffix && path.basename(absoluteModuleDir) === _.kebabCase(moduleName)) {
89
- const legacyModulePath = path.join(path.dirname(absoluteModuleDir), _.camelCase(moduleName), `${_.upperFirst(_.camelCase(moduleName))}${legacySuffix}.ts`);
90
- if (await getFileSystemEntryType(legacyModulePath)) {
91
- log.error(`Found ${chalkHighlightThing(legacyModulePath)} created with the pre-kebab-case naming convention. Rename it to ${chalkHighlightThing(absoluteModulePath)} manually or pass --out-dir to scaffold elsewhere.`);
92
- return process.exit(1);
93
- }
94
- }
95
84
  const prettiedCode = await prettify(code, absoluteModulePath);
96
85
  if (!dryRun) {
97
86
  if (!overwrite && (await getFileSystemEntryType(absoluteModulePath))) {
@@ -104,6 +93,9 @@ export async function newModule({ projectInfo, what, moduleNameWithOptionalSegme
104
93
  log.info(`${chalk.green('Created')}${empty ? ' empty' : ''} ${chalkHighlightThing(absoluteModulePath)} using ${chalkHighlightThing(`"${type}"`)} template for ${formatLoggedSegmentName(segmentName)}`);
105
94
  }
106
95
  }
96
+ else {
97
+ log.info(`Dry run: would create${empty ? ' empty' : ''} ${chalkHighlightThing(absoluteModulePath)} using ${chalkHighlightThing(`"${type}"`)} template for ${formatLoggedSegmentName(segmentName)}`);
98
+ }
107
99
  if (type === 'controller') {
108
100
  if (!sourceName) {
109
101
  throw new Error(`The template for "${type}" does not provide a sourceName`);
@@ -123,9 +115,12 @@ export async function newModule({ projectInfo, what, moduleNameWithOptionalSegme
123
115
  }), routeFilePath);
124
116
  if (!dryRun) {
125
117
  await fs.writeFile(routeFilePath, newSegmentCode);
118
+ log.info(`${chalk.green('Added')} ${chalkHighlightThing(sourceName)} ${type} as ${chalkHighlightThing(compiledName)} to ${formatLoggedSegmentName(segmentName)}`);
119
+ }
120
+ else {
121
+ log.info(`Dry run: would add ${chalkHighlightThing(sourceName)} ${type} as ${chalkHighlightThing(compiledName)} to ${formatLoggedSegmentName(segmentName)}`);
126
122
  }
127
123
  }
128
- log.info(`${chalk.green('Added')} ${chalkHighlightThing(sourceName)} ${type} as ${chalkHighlightThing(compiledName)} to ${formatLoggedSegmentName(segmentName)}`);
129
124
  }
130
125
  }
131
126
  }
@@ -1,10 +1,10 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
3
- import { getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
2
+ import path from 'node:path';
3
+ import chalk from 'chalk';
4
4
  import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
5
5
  import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
6
+ import { getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
6
7
  import { prettify } from '../utils/prettify.mjs';
7
- import chalk from 'chalk';
8
8
  export async function newSegment({ projectInfo, segmentName, isStaticSegment, overwrite, dryRun, }) {
9
9
  const { apiDirAbsolutePath, log, config } = projectInfo;
10
10
  if (!apiDirAbsolutePath) {
@@ -22,6 +22,8 @@ const controllers = {};
22
22
  export type Controllers = typeof controllers;
23
23
  ${isStaticSegment
24
24
  ? `
25
+ export const dynamic = 'force-static';
26
+
25
27
  export function generateStaticParams() {
26
28
  return controllersToStaticParams(controllers);
27
29
  }
@@ -35,8 +37,11 @@ ${segmentName ? ` segmentName: '${segmentName}',\n` : ''} emitSchema: true,
35
37
  if (!dryRun) {
36
38
  await fs.mkdir(path.dirname(absoluteSegmentRoutePath), { recursive: true });
37
39
  await fs.writeFile(absoluteSegmentRoutePath, code);
40
+ log.info(`${chalk.green('Created')} ${formatLoggedSegmentName(segmentName, { isStatic: isStaticSegment })} at ${absoluteSegmentRoutePath}.`);
41
+ }
42
+ else {
43
+ log.info(`Dry run: would create ${formatLoggedSegmentName(segmentName, { isStatic: isStaticSegment })} at ${absoluteSegmentRoutePath}.`);
38
44
  }
39
- log.info(`${chalk.green('Created')} ${formatLoggedSegmentName(segmentName, { isStatic: isStaticSegment })} at ${absoluteSegmentRoutePath}.`);
40
45
  const dir = chalk.cyanBright([segmentName, 'thing'].filter(Boolean).join('/'));
41
46
  log.info(`Run ${chalkHighlightThing(`npx vovk new service controller ${dir}`)} to create a new controller with a service at ./${path.join(config.modulesDir, dir)}/ directory for this segment`);
42
47
  }
@@ -1,9 +1,9 @@
1
+ import path from 'node:path';
1
2
  import ejs from 'ejs';
2
3
  import matter from 'gray-matter';
3
4
  import _ from 'lodash';
4
5
  import pluralize from 'pluralize';
5
6
  import { addCommonTerms } from './add-common-terms.mjs';
6
- import path from 'node:path';
7
7
  addCommonTerms();
8
8
  export async function render(codeTemplate, { cwd, config, withService, segmentName, moduleName, empty, templateFileName, isNodeNextResolution, srcRoot, }) {
9
9
  const defaultOutDir = [config.modulesDir, segmentName || config.rootSegmentModulesDirName, _.kebabCase(moduleName)]
package/dist/types.d.mts CHANGED
@@ -88,7 +88,7 @@ export type VovkEnv = {
88
88
  VOVK_API_ENTRY_POINT?: string;
89
89
  __VOVK_START_WATCHER_IN_STANDALONE_MODE__?: 'true';
90
90
  __VOVK_SCHEMA_OUT_FLAG__?: string;
91
- __VOVK_DEV_HTTPS_FLAG__?: 'true' | 'false';
91
+ __VOVK_DEV_HTTPS_FLAG__?: 'true' | '';
92
92
  __VOVK_EXIT__?: 'true' | 'false';
93
93
  __VOVK_LOG_LEVEL__?: LogLevelNames;
94
94
  };
@@ -1,5 +1,5 @@
1
- import type { OpenAPIObject } from 'openapi3-ts/oas31';
2
1
  import type { JSONSchema7 } from 'json-schema';
2
+ import type { OpenAPIObject } from 'openapi3-ts/oas31';
3
3
  export declare function compileJSONSchemaToTypeScriptType(schema: JSONSchema7, typeName: string, components?: NonNullable<OpenAPIObject['components']>, options?: {
4
4
  dontCreateRefTypes?: boolean;
5
5
  }): string;
@@ -1,5 +1,5 @@
1
- import upperFirst from 'lodash/upperFirst.js';
2
1
  import camelCase from 'lodash/camelCase.js';
2
+ import upperFirst from 'lodash/upperFirst.js';
3
3
  export function compileTs(options) {
4
4
  const context = {
5
5
  refs: options.refs || new Map(),
@@ -10,14 +10,5 @@ interface GenerateFnNameOptions {
10
10
  /** Segments to strip out (e.g. ['api','v1']) */
11
11
  ignoreSegments?: string[];
12
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
13
  export declare function generateFnName(method: HttpMethod, rawPath: string, opts?: GenerateFnNameOptions): string;
23
14
  export {};
@@ -15,15 +15,8 @@ export function capitalize(str) {
15
15
  const DEFAULT_OPTIONS = {
16
16
  ignoreSegments: ['api'],
17
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
- */
18
+ // HTTP method + OpenAPI path to a camelCased fn name,
19
+ // e.g. GET /users -> "listUsers", GET /users/{id} -> "getUsersById", POST /v1/api/orders -> "createOrders"
27
20
  export function generateFnName(method, rawPath, opts = {}) {
28
21
  const { ignoreSegments } = {
29
22
  ...DEFAULT_OPTIONS,
@@ -0,0 +1 @@
1
+ export declare const GENERATED_BANNER_PREFIX = "Generated by vovk-cli";
@@ -0,0 +1,3 @@
1
+ // stamped as the first line of every generated file that can hold a comment,
2
+ // the pruner relies on it to tell generated files from user files
3
+ export const GENERATED_BANNER_PREFIX = 'Generated by vovk-cli';
@@ -1,9 +1 @@
1
- /**
2
- * Finds an available port starting from a given port.
3
- * @param {number} startPort - The port to start checking from.
4
- * @param {number} maxAttempts - The maximum number of attempts to find an available port.
5
- * @param {number} attempt - The current attempt number.
6
- * @param {(failedPort: number, tryingPort: number) => void} onWarning - The callback function for warnings.
7
- * @returns {Promise<string>}
8
- */
9
1
  export declare function getAvailablePort(startPort: number, maxAttempts: number, attempt: number, onWarning: (failedPort: number, tryingPort: number) => void): Promise<string>;
@@ -1,10 +1,5 @@
1
1
  import net from 'node:net';
2
- // Created with AI
3
- /**
4
- * Checks if a port is available.
5
- * @param {number} port - The port to check.
6
- * @param {(isAvailable: boolean) => void} callback - The callback function.
7
- */
2
+ // checks if a port is free
8
3
  function checkPort(port, callback) {
9
4
  const server = net.createServer();
10
5
  server.listen(port, () => {
@@ -16,14 +11,7 @@ function checkPort(port, callback) {
16
11
  callback(false);
17
12
  });
18
13
  }
19
- /**
20
- * Finds an available port starting from a given port.
21
- * @param {number} startPort - The port to start checking from.
22
- * @param {number} maxAttempts - The maximum number of attempts to find an available port.
23
- * @param {number} attempt - The current attempt number.
24
- * @param {(failedPort: number, tryingPort: number) => void} onWarning - The callback function for warnings.
25
- * @returns {Promise<string>}
26
- */
14
+ // finds a free port starting from startPort, up to maxAttempts
27
15
  export function getAvailablePort(startPort, maxAttempts, attempt, onWarning) {
28
16
  return new Promise((resolve, reject) => {
29
17
  checkPort(startPort, (isAvailable) => {
@@ -1,5 +1,6 @@
1
- export declare enum FileSystemEntryType {
2
- FILE = "FILE",
3
- DIRECTORY = "DIRECTORY"
4
- }
1
+ export declare const FileSystemEntryType: {
2
+ readonly FILE: "FILE";
3
+ readonly DIRECTORY: "DIRECTORY";
4
+ };
5
+ export type FileSystemEntryType = (typeof FileSystemEntryType)[keyof typeof FileSystemEntryType];
5
6
  export declare function getFileSystemEntryType(filePath: string): Promise<FileSystemEntryType | null>;
@@ -1,9 +1,9 @@
1
1
  import fs from 'node:fs/promises';
2
- export var FileSystemEntryType;
3
- (function (FileSystemEntryType) {
4
- FileSystemEntryType["FILE"] = "FILE";
5
- FileSystemEntryType["DIRECTORY"] = "DIRECTORY";
6
- })(FileSystemEntryType || (FileSystemEntryType = {}));
2
+ // const object instead of enum, node 26 runs ts with erasable syntax only
3
+ export const FileSystemEntryType = {
4
+ FILE: 'FILE',
5
+ DIRECTORY: 'DIRECTORY',
6
+ };
7
7
  export async function getFileSystemEntryType(filePath) {
8
8
  try {
9
9
  const stats = await fs.stat(filePath);
@@ -1,6 +1,6 @@
1
+ import type { OpenAPIObject } from 'openapi3-ts/oas31';
1
2
  import type { HttpMethod, VovkConfig } from 'vovk';
2
3
  import type { VovkOperationObject, VovkStrictConfig } from 'vovk/internal';
3
- import type { OpenAPIObject } from 'openapi3-ts/oas31';
4
4
  import type { ProjectInfo } from '../get-project-info/index.mjs';
5
5
  export type GetOpenAPINameFn = (config: {
6
6
  operationObject: VovkOperationObject;
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import camelCase from 'lodash/camelCase.js';
3
4
  import * as YAML from 'yaml';
4
5
  import { chalkHighlightThing } from './chalk-highlight-thing.mjs';
5
- import camelCase from 'lodash/camelCase.js';
6
6
  import { generateFnName } from './generate-fn-name.mjs';
7
7
  const normalizeGetModuleName = (getModuleName) => {
8
+ getModuleName = getModuleName ?? 'api';
8
9
  if (typeof getModuleName === 'string') {
9
10
  const moduleName = getModuleName;
10
11
  getModuleName = () => moduleName;
@@ -15,6 +16,7 @@ const normalizeGetModuleName = (getModuleName) => {
15
16
  return getModuleName;
16
17
  };
17
18
  const normalizeGetMethodName = (getMethodName) => {
19
+ getMethodName = getMethodName ?? 'auto';
18
20
  if (getMethodName === 'camel-case-operation-id') {
19
21
  getMethodName = ({ operationObject }) => {
20
22
  const operationId = operationObject.operationId;
@@ -47,8 +49,21 @@ async function getOpenApiSpecLocal(openApiSpecFilePath, cwd) {
47
49
  return (fileName.endsWith('.json') ? JSON.parse(openApiSpecContent) : YAML.parse(openApiSpecContent));
48
50
  }
49
51
  async function getOpenApiSpecRemote({ cwd, url, fallback, log, }) {
50
- const resp = await fetch(url);
51
- const text = await resp.text();
52
+ let resp;
53
+ let text;
54
+ try {
55
+ resp = await fetch(url);
56
+ text = await resp.text();
57
+ }
58
+ catch (error) {
59
+ // network failures (DNS, offline, timeouts) should use the fallback too
60
+ const message = `Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${error.message}`;
61
+ if (fallback) {
62
+ log.warn(`${message}. Falling back to ${chalkHighlightThing(fallback)}`);
63
+ return getOpenApiSpecLocal(fallback, cwd);
64
+ }
65
+ throw new Error(message);
66
+ }
52
67
  if (!resp.ok) {
53
68
  if (fallback) {
54
69
  log.warn(`Failed to fetch OpenAPI spec from ${chalkHighlightThing(url)}: ${resp.status} ${resp.statusText}. Falling back to ${chalkHighlightThing(fallback)}`);
@@ -1 +1,5 @@
1
+ import type { getLogger } from './get-logger.mjs';
2
+ /** Formats with the project-installed prettier; returns the code unchanged when it's not installed. */
1
3
  export declare function prettify(code: string, absoluteFilePath: string): Promise<string>;
4
+ /** Warns once per run when prettifyClient is enabled but prettier is not installed. */
5
+ export declare function warnIfPrettierMissing(log: ReturnType<typeof getLogger>): Promise<void>;
@@ -1,10 +1,46 @@
1
- import prettier from 'prettier';
1
+ import { createRequire } from 'node:module';
2
+ import path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+ let prettierPromise;
5
+ let warned = false;
6
+ /** Prettier is not a CLI dependency; it's resolved from the user's project if installed. */
7
+ function getPrettier() {
8
+ prettierPromise ??= (async () => {
9
+ try {
10
+ const require = createRequire(path.join(process.cwd(), 'noop.js'));
11
+ const mod = (await import(pathToFileURL(require.resolve('prettier')).href));
12
+ return mod.default ?? mod;
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ })();
18
+ return prettierPromise;
19
+ }
20
+ /** Formats with the project-installed prettier; returns the code unchanged when it's not installed. */
2
21
  export async function prettify(code, absoluteFilePath) {
22
+ const prettier = await getPrettier();
23
+ if (!prettier)
24
+ return code;
3
25
  const options = await prettier.resolveConfig(absoluteFilePath);
4
26
  const finalOptions = {
5
27
  ...options,
6
28
  filepath: absoluteFilePath, // for selecting the correct parser
7
29
  };
8
- const formattedCode = await prettier.format(code, finalOptions);
9
- return formattedCode;
30
+ try {
31
+ return await prettier.format(code, finalOptions);
32
+ }
33
+ catch (error) {
34
+ // no parser for this file type (.rs, .toml, .py etc)
35
+ if (error.name === 'UndefinedParserError')
36
+ return code;
37
+ throw error;
38
+ }
39
+ }
40
+ /** Warns once per run when prettifyClient is enabled but prettier is not installed. */
41
+ export async function warnIfPrettierMissing(log) {
42
+ if (warned || (await getPrettier()))
43
+ return;
44
+ warned = true;
45
+ log.warn('prettifyClient is enabled but prettier is not installed. Either install it or set prettifyClient to false to suppress this warning.');
10
46
  }
@@ -1,9 +1 @@
1
- /**
2
- * Removes all directories in a folder that aren't in the provided allowlist
3
- * Supports nested directory paths like 'foo/bar/baz'
4
- *
5
- * @param folderPath - The path to the folder to process
6
- * @param allowedDirs - Array of relative directory paths to keep
7
- * @returns Promise that resolves when all operations are complete
8
- */
9
- export declare function removeUnlistedDirectories(folderPath: string, allowedDirs: string[]): Promise<void>;
1
+ export declare function removeUnlistedDirectories(folderPath: string, allowedDirs: string[], generatedRelPaths?: string[]): Promise<string[]>;