vovk-cli 0.1.3 → 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 (138) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/client-templates/{schemaTs → schema-ts}/schema.ts.ejs +1 -1
  3. package/client-templates/{tsBase → ts-base}/index.ts.ejs +1 -1
  4. package/dist/bundle/index.d.mts +1 -1
  5. package/dist/bundle/index.mjs +67 -45
  6. package/dist/dev/{diffSegmentSchema.d.mts → diff-segment-schema.d.mts} +0 -17
  7. package/dist/dev/{diffSegmentSchema.mjs → diff-segment-schema.mjs} +1 -17
  8. package/dist/dev/{ensureSchemaFiles.d.mts → ensure-schema-files.d.mts} +1 -1
  9. package/dist/dev/{ensureSchemaFiles.mjs → ensure-schema-files.mjs} +30 -12
  10. package/dist/dev/index.mjs +23 -17
  11. package/dist/dev/log-diff-result.d.mts +3 -0
  12. package/dist/dev/{logDiffResult.mjs → log-diff-result.mjs} +2 -2
  13. package/dist/dev/{writeMetaJson.d.mts → write-meta-json.d.mts} +1 -1
  14. package/dist/dev/{writeMetaJson.mjs → write-meta-json.mjs} +3 -3
  15. package/dist/dev/{writeOneSegmentSchemaFile.d.mts → write-one-segment-schema-file.d.mts} +1 -1
  16. package/dist/dev/{writeOneSegmentSchemaFile.mjs → write-one-segment-schema-file.mjs} +8 -3
  17. package/dist/generate/ensure-client.d.mts +3 -0
  18. package/dist/generate/{ensureClient.mjs → ensure-client.mjs} +1 -1
  19. package/dist/generate/generate.d.mts +2 -2
  20. package/dist/generate/generate.mjs +41 -28
  21. package/dist/generate/{getClientTemplateFiles.d.mts → get-client-template-files.d.mts} +1 -1
  22. package/dist/generate/{getClientTemplateFiles.mjs → get-client-template-files.mjs} +8 -4
  23. package/dist/generate/{getProjectFullSchema.d.mts → get-project-full-schema.d.mts} +1 -1
  24. package/dist/generate/{getProjectFullSchema.mjs → get-project-full-schema.mjs} +3 -3
  25. package/dist/generate/{getTemplateClientImports.mjs → get-template-client-imports.mjs} +2 -2
  26. package/dist/generate/index.d.mts +2 -2
  27. package/dist/generate/index.mjs +46 -34
  28. package/dist/generate/{writeOneClientFile.d.mts → write-one-client-file.d.mts} +5 -5
  29. package/dist/generate/{writeOneClientFile.mjs → write-one-client-file.mjs} +10 -8
  30. package/dist/{getProjectInfo/getConfig/getRelativeSrcRoot.mjs → get-project-info/get-config/get-relative-src-root.mjs} +1 -1
  31. package/dist/get-project-info/get-config/get-template-defs.d.mts +22 -0
  32. package/dist/{getProjectInfo/getConfig/getTemplateDefs.mjs → get-project-info/get-config/get-template-defs.mjs} +35 -61
  33. package/dist/{getProjectInfo/getConfig/getUserConfig.mjs → get-project-info/get-config/get-user-config.mjs} +2 -2
  34. package/dist/{getProjectInfo/getConfig → get-project-info/get-config}/index.d.mts +2 -2
  35. package/dist/{getProjectInfo/getConfig → get-project-info/get-config}/index.mjs +17 -12
  36. package/dist/{getProjectInfo/getMetaSchema.d.mts → get-project-info/get-meta-schema.d.mts} +1 -1
  37. package/dist/{getProjectInfo → get-project-info}/index.mjs +3 -3
  38. package/dist/index.mjs +25 -16
  39. package/dist/init/{createConfig.d.mts → create-config.d.mts} +1 -1
  40. package/dist/init/{createConfig.mjs → create-config.mjs} +6 -6
  41. package/dist/init/index.d.mts +1 -1
  42. package/dist/init/index.mjs +54 -22
  43. package/dist/init/{installDependencies.d.mts → install-dependencies.d.mts} +2 -1
  44. package/dist/init/{installDependencies.mjs → install-dependencies.mjs} +11 -3
  45. package/dist/init/{logUpdateDependenciesError.d.mts → log-update-dependencies-error.d.mts} +2 -2
  46. package/dist/init/{logUpdateDependenciesError.mjs → log-update-dependencies-error.mjs} +1 -1
  47. package/dist/init/{updateDependenciesWithoutInstalling.d.mts → update-dependencies-without-installing.d.mts} +1 -1
  48. package/dist/init/{updateDependenciesWithoutInstalling.mjs → update-dependencies-without-installing.mjs} +1 -2
  49. package/dist/init/update-gitignore.d.mts +2 -0
  50. package/dist/init/update-gitignore.mjs +20 -0
  51. package/dist/new/index.d.mts +1 -1
  52. package/dist/new/index.mjs +2 -2
  53. package/dist/new/{newModule.d.mts → new-module.d.mts} +1 -1
  54. package/dist/new/{newModule.mjs → new-module.mjs} +15 -9
  55. package/dist/new/{newSegment.d.mts → new-segment.d.mts} +1 -1
  56. package/dist/new/{newSegment.mjs → new-segment.mjs} +11 -6
  57. package/dist/new/render.mjs +3 -3
  58. package/dist/types.d.mts +1 -1
  59. package/dist/utils/{compileJSONSchemaToTypeScriptType.mjs → compile-json-schema-to-typescript-type.mjs} +1 -1
  60. package/dist/utils/{formatLoggedSegmentName.mjs → format-logged-segment-name.mjs} +1 -1
  61. package/dist/utils/{generateFnName.d.mts → generate-fn-name.d.mts} +0 -9
  62. package/dist/utils/{generateFnName.mjs → generate-fn-name.mjs} +2 -9
  63. package/dist/utils/generated-banner.d.mts +1 -0
  64. package/dist/utils/generated-banner.mjs +3 -0
  65. package/dist/utils/get-available-port.d.mts +1 -0
  66. package/dist/utils/{getAvailablePort.mjs → get-available-port.mjs} +2 -14
  67. package/dist/utils/get-file-system-entry-type.d.mts +6 -0
  68. package/dist/utils/{getFileSystemEntryType.mjs → get-file-system-entry-type.mjs} +5 -5
  69. package/dist/utils/{getPackageJson.d.mts → get-package-json.d.mts} +1 -1
  70. package/dist/utils/{getPackageJson.mjs → get-package-json.mjs} +1 -1
  71. package/dist/utils/{locateSegments.d.mts → locate-segments.d.mts} +1 -1
  72. package/dist/utils/{locateSegments.mjs → locate-segments.mjs} +1 -1
  73. package/dist/utils/{normalizeOpenAPIMixin.d.mts → normalize-openapi-mixin.d.mts} +2 -2
  74. package/dist/utils/{normalizeOpenAPIMixin.mjs → normalize-openapi-mixin.mjs} +20 -5
  75. package/dist/utils/prettify.d.mts +4 -0
  76. package/dist/utils/prettify.mjs +39 -3
  77. package/dist/utils/remove-unlisted-directories.d.mts +1 -0
  78. package/dist/utils/remove-unlisted-directories.mjs +99 -0
  79. package/dist/utils/{resolveAbsoluteModulePath.mjs → resolve-absolute-module-path.mjs} +2 -3
  80. package/dist/utils/{updateConfigProperty.mjs → update-config-property.mjs} +1 -1
  81. package/module-templates/arktype/controller.ts.ejs +2 -2
  82. package/module-templates/type/controller.ts.ejs +2 -2
  83. package/module-templates/type/service.ts.ejs +2 -2
  84. package/module-templates/valibot/controller.ts.ejs +2 -2
  85. package/module-templates/zod/controller.ts.ejs +2 -2
  86. package/package.json +23 -30
  87. package/client-templates/jsBase/index.d.ts.ejs +0 -21
  88. package/client-templates/jsBase/index.js.ejs +0 -18
  89. package/client-templates/openapiJs/openapi.d.ts.ejs +0 -4
  90. package/client-templates/openapiJs/openapi.js.ejs +0 -4
  91. package/client-templates/schemaJs/schema.d.ts.ejs +0 -10
  92. package/client-templates/schemaJs/schema.js.ejs +0 -29
  93. package/dist/dev/logDiffResult.d.mts +0 -3
  94. package/dist/generate/ensureClient.d.mts +0 -3
  95. package/dist/getProjectInfo/getConfig/getTemplateDefs.d.mts +0 -25
  96. package/dist/utils/getAvailablePort.d.mts +0 -9
  97. package/dist/utils/getFileSystemEntryType.d.mts +0 -5
  98. package/dist/utils/removeUnlistedDirectories.d.mts +0 -9
  99. package/dist/utils/removeUnlistedDirectories.mjs +0 -60
  100. package/client-templates/{openapiJson → openapi-json}/openapi.json.ejs +0 -0
  101. package/client-templates/{openapiTs → openapi-ts}/openapi.ts.ejs +0 -0
  102. package/client-templates/{packageJson → package-json}/package.json.ejs +0 -0
  103. package/client-templates/{schemaJson → schema-json}/schema.json.ejs +0 -0
  104. package/dist/generate/{getTemplateClientImports.d.mts → get-template-client-imports.d.mts} +0 -0
  105. package/dist/{getProjectInfo/getConfig/getConfigAbsolutePaths.d.mts → get-project-info/get-config/get-config-absolute-paths.d.mts} +0 -0
  106. package/dist/{getProjectInfo/getConfig/getConfigAbsolutePaths.mjs → get-project-info/get-config/get-config-absolute-paths.mjs} +0 -0
  107. package/dist/{getProjectInfo/getConfig/getRelativeSrcRoot.d.mts → get-project-info/get-config/get-relative-src-root.d.mts} +0 -0
  108. package/dist/{getProjectInfo/getConfig/getUserConfig.d.mts → get-project-info/get-config/get-user-config.d.mts} +0 -0
  109. package/dist/{getProjectInfo/getMetaSchema.mjs → get-project-info/get-meta-schema.mjs} +1 -1
  110. package/dist/{getProjectInfo → get-project-info}/index.d.mts +0 -0
  111. package/dist/init/{checkTSConfigForExperimentalDecorators.d.mts → check-tsconfig-for-experimental-decorators.d.mts} +0 -0
  112. package/dist/init/{checkTSConfigForExperimentalDecorators.mjs → check-tsconfig-for-experimental-decorators.mjs} +1 -1
  113. package/dist/init/{updateNPMScripts.d.mts → update-npm-scripts.d.mts} +0 -0
  114. package/dist/init/{updateNPMScripts.mjs → update-npm-scripts.mjs} +0 -0
  115. package/dist/init/{updateTypeScriptConfig.d.mts → update-typescript-config.d.mts} +0 -0
  116. package/dist/init/{updateTypeScriptConfig.mjs → update-typescript-config.mjs} +1 -1
  117. package/dist/new/{addClassToSegmentCode.d.mts → add-class-to-segment-code.d.mts} +0 -0
  118. package/dist/new/{addClassToSegmentCode.mjs → add-class-to-segment-code.mjs} +0 -0
  119. package/dist/new/{addCommonTerms.d.mts → add-common-terms.d.mts} +0 -0
  120. package/dist/new/{addCommonTerms.mjs → add-common-terms.mjs} +0 -0
  121. package/dist/utils/{chalkHighlightThing.d.mts → chalk-highlight-thing.d.mts} +0 -0
  122. package/dist/utils/{chalkHighlightThing.mjs → chalk-highlight-thing.mjs} +0 -0
  123. package/dist/utils/{compileJSONSchemaToTypeScriptType.d.mts → compile-json-schema-to-typescript-type.d.mts} +1 -1
  124. package/dist/utils/{compileTs.d.mts → compile-ts.d.mts} +0 -0
  125. package/dist/utils/{compileTs.mjs → compile-ts.mjs} +1 -1
  126. /package/dist/utils/{debounceWithArgs.d.mts → debounce-with-args.d.mts} +0 -0
  127. /package/dist/utils/{debounceWithArgs.mjs → debounce-with-args.mjs} +0 -0
  128. /package/dist/utils/{formatLoggedSegmentName.d.mts → format-logged-segment-name.d.mts} +0 -0
  129. /package/dist/utils/{getLogger.d.mts → get-logger.d.mts} +0 -0
  130. /package/dist/utils/{getLogger.mjs → get-logger.mjs} +0 -0
  131. /package/dist/utils/{getNPMPackageMetadata.d.mts → get-npm-package-metadata.d.mts} +0 -0
  132. /package/dist/utils/{getNPMPackageMetadata.mjs → get-npm-package-metadata.mjs} +0 -0
  133. /package/dist/utils/{getPublicModuleNameFromPath.d.mts → get-public-module-name-from-path.d.mts} +0 -0
  134. /package/dist/utils/{getPublicModuleNameFromPath.mjs → get-public-module-name-from-path.mjs} +0 -0
  135. /package/dist/utils/{pickSegmentFullSchema.d.mts → pick-segment-full-schema.d.mts} +0 -0
  136. /package/dist/utils/{pickSegmentFullSchema.mjs → pick-segment-full-schema.mjs} +0 -0
  137. /package/dist/utils/{resolveAbsoluteModulePath.d.mts → resolve-absolute-module-path.d.mts} +0 -0
  138. /package/dist/utils/{updateConfigProperty.d.mts → update-config-property.d.mts} +0 -0
@@ -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) => {
@@ -0,0 +1,6 @@
1
+ export declare const FileSystemEntryType: {
2
+ readonly FILE: "FILE";
3
+ readonly DIRECTORY: "DIRECTORY";
4
+ };
5
+ export type FileSystemEntryType = (typeof FileSystemEntryType)[keyof typeof FileSystemEntryType];
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,3 +1,3 @@
1
1
  import type { PackageJson } from 'type-fest';
2
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ import type { ProjectInfo } from '../get-project-info/index.mjs';
3
3
  export declare function getPackageJson(cwd: string, log: ProjectInfo['log']): Promise<PackageJson>;
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { chalkHighlightThing } from './chalkHighlightThing.mjs';
3
+ import { chalkHighlightThing } from './chalk-highlight-thing.mjs';
4
4
  let cachedPromise;
5
5
  export function getPackageJson(cwd, log) {
6
6
  const pkgPath = path.join(cwd, 'package.json');
@@ -1,5 +1,5 @@
1
1
  import type { VovkStrictConfig } from 'vovk/internal';
2
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ import type { ProjectInfo } from '../get-project-info/index.mjs';
3
3
  export type Segment = {
4
4
  routeFilePath: string;
5
5
  segmentName: string;
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { getFileSystemEntryType } from './getFileSystemEntryType.mjs';
3
+ import { getFileSystemEntryType } from './get-file-system-entry-type.mjs';
4
4
  export async function locateSegments({ dir, rootDir, config, log, }) {
5
5
  let results = [];
6
6
  if (!dir)
@@ -1,7 +1,7 @@
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
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
4
+ import type { ProjectInfo } from '../get-project-info/index.mjs';
5
5
  export type GetOpenAPINameFn = (config: {
6
6
  operationObject: VovkOperationObject;
7
7
  method: HttpMethod;
@@ -1,10 +1,11 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import * as YAML from 'yaml';
4
- import { chalkHighlightThing } from './chalkHighlightThing.mjs';
5
3
  import camelCase from 'lodash/camelCase.js';
6
- import { generateFnName } from './generateFnName.mjs';
4
+ import * as YAML from 'yaml';
5
+ import { chalkHighlightThing } from './chalk-highlight-thing.mjs';
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
  }
@@ -0,0 +1 @@
1
+ export declare function removeUnlistedDirectories(folderPath: string, allowedDirs: string[], generatedRelPaths?: string[]): Promise<string[]>;
@@ -0,0 +1,99 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { GENERATED_BANNER_PREFIX } from './generated-banner.mjs';
4
+ import { FileSystemEntryType, getFileSystemEntryType } from './get-file-system-entry-type.mjs';
5
+ // removes all dirs in folderPath that aren't in allowedDirs, supports nested paths like 'foo/bar/baz'
6
+ // generatedRelPaths guards user files: a dir holding anything the generator wouldn't write is kept,
7
+ // returns the dirs that were kept for that reason
8
+ export async function removeUnlistedDirectories(folderPath, allowedDirs, generatedRelPaths) {
9
+ // Normalize all allowed paths to use the system-specific separator
10
+ const normalizedAllowedDirs = allowedDirs.map((dir) => dir.split('/').join(path.sep));
11
+ const skipped = [];
12
+ // Process the directory tree recursively
13
+ await processDirectory(folderPath, '', normalizedAllowedDirs, generatedRelPaths, skipped);
14
+ return skipped;
15
+ }
16
+ // "[package_name]" is substituted at write time, so treat it as a wildcard segment
17
+ function matchesGeneratedPath(relPath, generatedRelPaths) {
18
+ const segments = relPath.split(path.sep);
19
+ return generatedRelPaths.some((generated) => {
20
+ const generatedSegments = generated.split(path.sep);
21
+ if (generatedSegments.length > segments.length)
22
+ return false;
23
+ // a nested segment such as bar/baz adds leading directories, so match the tail
24
+ const offset = segments.length - generatedSegments.length;
25
+ return generatedSegments.every((segment, i) => segment === '[package_name]' || segment === segments[offset + i]);
26
+ });
27
+ }
28
+ // a matching name is not enough, a user file may be named like ours, so require our banner too.
29
+ // json holds no comment and therefore no banner, so there the name stays the only signal
30
+ async function isGeneratedFile(absolutePath, relPath, generatedRelPaths) {
31
+ if (!matchesGeneratedPath(relPath, generatedRelPaths))
32
+ return false;
33
+ if (path.extname(absolutePath) === '.json')
34
+ return true;
35
+ try {
36
+ const content = await fs.readFile(absolutePath, 'utf-8');
37
+ return content.slice(0, content.indexOf('\n') + 1 || undefined).includes(GENERATED_BANNER_PREFIX);
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ // true when every file below dirPath is something the generator wrote
44
+ async function containsOnlyGenerated(dirPath, generatedRelPaths, relativePath = '') {
45
+ const entries = await fs.readdir(path.join(dirPath, relativePath), { withFileTypes: true });
46
+ for (const entry of entries) {
47
+ const entryRelPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
48
+ if (entry.isDirectory()) {
49
+ if (!(await containsOnlyGenerated(dirPath, generatedRelPaths, entryRelPath)))
50
+ return false;
51
+ }
52
+ else if (!(await isGeneratedFile(path.join(dirPath, entryRelPath), entryRelPath, generatedRelPaths))) {
53
+ return false;
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ // recursively decides which dirs to keep or remove
59
+ async function processDirectory(basePath, relativePath, allowedDirs, generatedRelPaths, skipped) {
60
+ const currentDirPath = path.join(basePath, relativePath);
61
+ // check if the current path is a directory
62
+ const type = await getFileSystemEntryType(currentDirPath);
63
+ if (type !== FileSystemEntryType.DIRECTORY) {
64
+ // If it's not a directory, return early
65
+ return;
66
+ }
67
+ // Read all entries in the current directory
68
+ const entries = await fs.readdir(currentDirPath, { withFileTypes: true });
69
+ // Process only directories
70
+ const dirEntries = entries.filter((entry) => entry.isDirectory());
71
+ // Check each directory
72
+ for (const dir of dirEntries) {
73
+ // Calculate the new relative path
74
+ const newRelativePath = relativePath ? path.join(relativePath, dir.name) : dir.name;
75
+ // Check if this directory or any of its subdirectories should be kept
76
+ const shouldKeep = allowedDirs.some((allowedDir) => {
77
+ // Direct match
78
+ if (allowedDir === newRelativePath)
79
+ return true;
80
+ // Check if it's a parent path of an allowed directory
81
+ // e.g. "foo" is a parent of "foo/bar/baz"
82
+ return allowedDir.startsWith(newRelativePath + path.sep);
83
+ });
84
+ if (shouldKeep) {
85
+ // Recursively process this directory's contents
86
+ await processDirectory(basePath, newRelativePath, allowedDirs, generatedRelPaths, skipped);
87
+ }
88
+ else {
89
+ const fullPath = path.join(basePath, newRelativePath);
90
+ // never delete a directory that holds files we did not generate
91
+ if (generatedRelPaths && !(await containsOnlyGenerated(fullPath, generatedRelPaths))) {
92
+ skipped.push(fullPath);
93
+ continue;
94
+ }
95
+ // Remove this directory since it's not in the allowed list
96
+ await fs.rm(fullPath, { recursive: true, force: true });
97
+ }
98
+ }
99
+ }
@@ -1,6 +1,6 @@
1
- import path from 'node:path';
2
1
  import { createRequire } from 'node:module';
3
- import { getPublicModuleNameFromPath } from './getPublicModuleNameFromPath.mjs';
2
+ import path from 'node:path';
3
+ import { getPublicModuleNameFromPath } from './get-public-module-name-from-path.mjs';
4
4
  // Returns the path up to and including the last occurrence of the given module name
5
5
  export function getPathUpToModule(moduleName, fullPath) {
6
6
  const idx = fullPath.lastIndexOf(moduleName);
@@ -24,7 +24,6 @@ export function resolveAbsoluteModulePath(modulePath, cwd) {
24
24
  return path.resolve(getPathUpToModule(moduleName, path.dirname(resolved)), restPath);
25
25
  }
26
26
  catch (e) {
27
- // eslint-disable-next-line no-console
28
27
  console.error(`Error resolving module path: ${modulePath}`, e);
29
28
  // If resolution fails, fall back to the original behavior
30
29
  return path.resolve(cwd, './node_modules', modulePath);
@@ -1,4 +1,4 @@
1
- import { Project, QuoteKind, IndentationText, NewLineKind, SyntaxKind, Node, } from 'ts-morph';
1
+ import { IndentationText, NewLineKind, Node, Project, QuoteKind, SyntaxKind, } from 'ts-morph';
2
2
  export function updateConfigProperty(sourceCode, pathToProperty, newValue) {
3
3
  const project = createProject();
4
4
  const sourceFile = project.createSourceFile('config-temp.mts', sourceCode, { overwrite: true });
@@ -4,7 +4,7 @@
4
4
  }; %>
5
5
  ---
6
6
  outDir: <%= t.defaultOutDir %>
7
- fileName: <%= vars.ModuleName + '.ts' %>
7
+ fileName: <%= t['the-thing'] + '-controller.ts' %>
8
8
  sourceName: <%= vars.ModuleName %>
9
9
  compiledName: <%= t.TheThing + 'RPC' %>
10
10
  ---
@@ -12,7 +12,7 @@ compiledName: <%= t.TheThing + 'RPC' %>
12
12
  import { procedure, prefix, get, put, post, del, operation } from 'vovk';
13
13
  import { type } from 'arktype';
14
14
  <% if(t.withService) { %>
15
- import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
15
+ import <%= vars.ServiceName %> from './<%= t['the-thing'] %>-service<%= t.nodeNextResolutionExt.ts %>';
16
16
  <% } %>
17
17
 
18
18
  @prefix('<%= t['the-things'] %>')
@@ -4,14 +4,14 @@
4
4
  }; %>
5
5
  ---
6
6
  outDir: <%= t.defaultOutDir %>
7
- fileName: <%= vars.ControllerName + '.ts' %>
7
+ fileName: <%= t['the-thing'] + '-controller.ts' %>
8
8
  sourceName: <%= vars.ControllerName %>
9
9
  compiledName: <%= t.TheThing + 'RPC' %>
10
10
  ---
11
11
 
12
12
  import { prefix, get, put, post, del, operation, type VovkRequest } from 'vovk';
13
13
  <% if(t.withService) { %>
14
- import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
14
+ import <%= vars.ServiceName %> from './<%= t['the-thing'] %>-service<%= t.nodeNextResolutionExt.ts %>';
15
15
  <% } %>
16
16
 
17
17
  @prefix('<%= t['the-things'] %>')
@@ -4,12 +4,12 @@
4
4
  }; %>
5
5
  ---
6
6
  outDir: <%= t.defaultOutDir %>
7
- fileName: <%= vars.ServiceName + '.ts' %>
7
+ fileName: <%= t['the-thing'] + '-service.ts' %>
8
8
  sourceName: <%= vars.ServiceName %>
9
9
  ---
10
10
 
11
11
  import type { VovkBody, VovkParams } from 'vovk';
12
- import type <%= vars.ControllerName %> from './<%= vars.ControllerName %><%= t.nodeNextResolutionExt.ts %>';
12
+ import type <%= vars.ControllerName %> from './<%= t['the-thing'] %>-controller<%= t.nodeNextResolutionExt.ts %>';
13
13
 
14
14
  export default class <%= vars.ServiceName %> {
15
15
  static get<%= t.TheThings %> = () => {
@@ -4,7 +4,7 @@
4
4
  }; %>
5
5
  ---
6
6
  outDir: <%= t.defaultOutDir %>
7
- fileName: <%= vars.ModuleName + '.ts' %>
7
+ fileName: <%= t['the-thing'] + '-controller.ts' %>
8
8
  sourceName: <%= vars.ModuleName %>
9
9
  compiledName: <%= t.TheThing + 'RPC' %>
10
10
  ---
@@ -13,7 +13,7 @@ import { procedure, prefix, get, put, post, del, operation } from 'vovk';
13
13
  import * as v from 'valibot';
14
14
  import { toStandardJsonSchema } from '@valibot/to-json-schema';
15
15
  <% if(t.withService) { %>
16
- import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
16
+ import <%= vars.ServiceName %> from './<%= t['the-thing'] %>-service<%= t.nodeNextResolutionExt.ts %>';
17
17
  <% } %>
18
18
 
19
19
  @prefix('<%= t['the-things'] %>')
@@ -4,7 +4,7 @@
4
4
  }; %>
5
5
  ---
6
6
  outDir: <%= t.defaultOutDir %>
7
- fileName: <%= vars.ModuleName + '.ts' %>
7
+ fileName: <%= t['the-thing'] + '-controller.ts' %>
8
8
  sourceName: <%= vars.ModuleName %>
9
9
  compiledName: <%= t.TheThing + 'RPC' %>
10
10
  ---
@@ -12,7 +12,7 @@ compiledName: <%= t.TheThing + 'RPC' %>
12
12
  import { procedure, prefix, get, put, post, del, operation } from 'vovk';
13
13
  import { z } from 'zod';
14
14
  <% if(t.withService) { %>
15
- import <%= vars.ServiceName %> from './<%= vars.ServiceName %><%= t.nodeNextResolutionExt.ts %>';
15
+ import <%= vars.ServiceName %> from './<%= t['the-thing'] %>-service<%= t.nodeNextResolutionExt.ts %>';
16
16
  <% } %>
17
17
 
18
18
  @prefix('<%= t['the-things'] %>')
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.1.3",
3
+ "version": "0.3.0-beta.0",
4
4
  "description": "CLI tool for managing Vovk.ts projects",
5
5
  "files": [
6
6
  "dist",
7
7
  "client-templates",
8
- "module-templates"
8
+ "module-templates",
9
+ "CHANGELOG.md"
9
10
  ],
10
11
  "bin": {
11
12
  "vovk": "./dist/index.mjs"
@@ -18,10 +19,10 @@
18
19
  "build": "rm -rf dist tsconfig.build.tsbuildinfo && tsc -P tsconfig.build.json",
19
20
  "postbuild": "chmod +x ./dist/index.mjs",
20
21
  "pre-test": "npm run build && npm run clear-test-cache",
21
- "test-only": "npm run pre-test && NODE_ENV=test node --experimental-transform-types --experimental-strip-types --experimental-vm-modules --test --test-only test/spec/**/*.mts",
22
- "test": "npm run pre-test && NODE_ENV=test node --experimental-transform-types --experimental-strip-types --experimental-vm-modules --test --test-concurrency=1 test/spec/**/*.mts",
22
+ "test-only": "npm run pre-test && NODE_ENV=test node --experimental-strip-types --experimental-vm-modules --test --test-only test/spec/**/*.mts",
23
+ "test": "npm run pre-test && NODE_ENV=test node --experimental-strip-types --experimental-vm-modules --test --test-concurrency=1 test/spec/**/*.mts",
23
24
  "tsc": "tsc --noEmit",
24
- "ncu": "npm-check-updates -u -x commander,node-pty",
25
+ "ncu": "npm-check-updates -u -x commander,tsdown",
25
26
  "npm-publish": "npm publish",
26
27
  "clear-test-cache": "rm -rf ./tmp_*"
27
28
  },
@@ -44,36 +45,29 @@
44
45
  "peerDependencies": {
45
46
  "vovk": ">=3.0.0"
46
47
  },
47
- "optionalDependencies": {
48
- "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13"
49
- },
50
48
  "dependencies": {
51
49
  "@iarna/toml": "^2.2.5",
52
- "@inquirer/prompts": "^8.3.2",
53
- "@npmcli/package-json": "^7.0.5",
50
+ "@inquirer/prompts": "^8.5.2",
51
+ "@npmcli/package-json": "^8.0.0",
54
52
  "@types/json-schema": "^7.0.15",
55
- "@types/lodash": "^4.17.24",
56
- "chalk": "^5.6.2",
53
+ "@types/lodash": "^4.17.25",
54
+ "chalk": "^6.0.0",
57
55
  "chokidar": "^5.0.0",
58
- "clone-deep": "^4.0.1",
59
56
  "commander": "^13.1.0",
60
- "concurrently": "^9.2.1",
61
- "dotenv": "^17.4.1",
62
- "ejs": "^5.0.1",
63
- "get-tsconfig": "^4.13.7",
57
+ "concurrently": "^10.0.4",
58
+ "dotenv": "^17.4.2",
59
+ "ejs": "^6.0.1",
60
+ "get-tsconfig": "^4.14.1",
64
61
  "glob": "^13.0.6",
65
62
  "gray-matter": "^4.0.3",
66
- "inflection": "^3.0.2",
67
63
  "jsonc-parser": "^3.3.1",
68
64
  "lodash": "^4.18.1",
69
65
  "loglevel": "^1.9.2",
70
66
  "pluralize": "^8.0.0",
71
- "prettier": "^3.8.1",
72
- "tar-stream": "^3.1.8",
73
- "ts-morph": "^27.0.2",
74
- "type-fest": "^5.5.0",
75
- "undici": "^8.0.2",
76
- "yaml": "^2.8.3"
67
+ "ts-morph": "^28.0.0",
68
+ "type-fest": "^5.8.0",
69
+ "undici": "^8.9.0",
70
+ "yaml": "^2.9.0"
77
71
  },
78
72
  "devDependencies": {
79
73
  "@types/concat-stream": "^2.0.3",
@@ -81,13 +75,12 @@
81
75
  "@types/js-yaml": "^4.0.9",
82
76
  "@types/npmcli__package-json": "^4.0.4",
83
77
  "@types/pluralize": "^0.0.33",
84
- "@types/tar-stream": "^3.1.4",
85
78
  "concat-stream": "^2.0.0",
86
- "create-next-app": "^16.2.2",
79
+ "create-next-app": "^16.2.12",
87
80
  "http-server": "^14.1.1",
88
- "node-pty": "0.10.1",
89
- "openapi3-ts": "^4.5.0",
90
- "tsdown": "^0.21.7",
91
- "zod": "^4.3.6"
81
+ "node-pty": "1.1.0",
82
+ "openapi3-ts": "^4.6.1",
83
+ "tsdown": "0.22.14",
84
+ "zod": "^4.4.3"
92
85
  }
93
86
  }
@@ -1,21 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- import type { VovkFetcher } from 'vovk/fetcher';
3
- import type { createRPC } from '<%= t.commonImports.createRPC %>';
4
- <% Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach((segment, i) => { if(segment.segmentType !== 'mixin') { %>
5
- import type { Controllers as Controllers<%= i %> } from "<%= t.segmentMeta[segment.segmentName].segmentImportPath %>";
6
- <% }}) %>
7
- <% if (t.hasMixins) { %>
8
- import type { Controllers as MixinControllers, Mixins } from "./mixins";
9
- <% } %>
10
- <% Object.entries(t.reExports).forEach(([reExportWhat, reExportFrom]) => { %>
11
- export { <%= reExportWhat %> } from '<%= reExportFrom %>';
12
- <% }) %>
13
- <% Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach((segment, i) => { %>
14
- <% Object.keys(segment.controllers).forEach((rpcModuleName) => { %>
15
- export const <%= rpcModuleName %>: ReturnType<typeof createRPC<<%= segment.segmentType === 'mixin' ? `MixinControllers` : `Controllers${i}` %>["<%= rpcModuleName %>"], typeof import('<%- t.segmentImports[segment.segmentName].fetcher %>').fetcher extends VovkFetcher<infer U> ? U : never>>;
16
- <% })
17
- }) %>
18
- export { schema } from './schema.js';
19
- <% if (t.hasMixins) { %>
20
- export { Mixins };
21
- <% } %>
@@ -1,18 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- import { createRPC } from '<%= t.commonImports.createRPC %>';
3
- import { schema } from './schema.js';
4
- <% Object.entries(t.reExports).forEach(([reExportWhat, reExportFrom]) => { %>
5
- export { <%= reExportWhat %> } from '<%= reExportFrom %>';
6
- <% }) %>
7
- <% Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach((segment, i) => { %>
8
- <% Object.keys(segment.controllers).forEach((rpcModuleName) => {
9
- const apiRoot = t.segmentMeta[segment.segmentName].forceApiRoot ?? t.apiRoot; %>
10
- export const <%= rpcModuleName %> = createRPC(
11
- schema, '<%= segment.segmentName %>', '<%= rpcModuleName %>', import('<%- t.segmentImports[segment.segmentName].fetcher %>'),
12
- { validateOnClient: <%- t.segmentImports[segment.segmentName].validateOnClient ? `import('${t.segmentImports[segment.segmentName].validateOnClient}')` : 'undefined' %>, <%- typeof t.segmentMeta[segment.segmentName].segmentNameOverride === 'string' ? `segmentNameOverride: '${t.segmentMeta[segment.segmentName].segmentNameOverride}', ` : '' %><%- segment.segmentType === 'mixin' ? '' : apiRoot ? `apiRoot: '${apiRoot}'` : '' %> }
13
- );
14
- <%
15
- });
16
- });
17
- %>
18
- export { schema };
@@ -1,4 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- import type { OpenAPIObject } from 'openapi3-ts/oas31';
3
-
4
- export const openapi: OpenAPIObject;
@@ -1,4 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- import openapi from './openapi.json' with { type: 'json' };
3
-
4
- export { openapi };
@@ -1,10 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- import type { VovkMetaSchema, VovkSegmentSchema } from 'vovk';
3
-
4
- export const schema: {
5
- $schema: '<%- t.VovkSchemaIdEnum.SCHEMA %>';
6
- meta: VovkMetaSchema;
7
- segments: {<% Object.values(t.schema.segments).filter((segment) => segment.emitSchema).forEach((segment) => { %>
8
- '<%= segment.segmentName %>': VovkSegmentSchema;<% }) %>
9
- };
10
- };
@@ -1,29 +0,0 @@
1
- <%- t.getFirstLineBanner() %>
2
- <% if(t.isVovkProject) { %>
3
- import meta from './<%= t.schemaOutDir %>/_meta.json' with { type: 'json' };
4
- <% } else { %>
5
- const meta = <%- JSON.stringify(t.schema.meta, null, 2) %>;
6
- <% } %>
7
-
8
- <% if(t.hasMixins) { %>
9
- import mixins from './mixins.json' with { type: 'json' };
10
- <% } %>
11
- <% Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType !== 'mixin').forEach((segment, i) => { %>
12
- import schema<%= i %> from './<%= t.schemaOutDir %>/<%= segment.segmentName || t.ROOT_SEGMENT_FILE_NAME %>.json' with { type: 'json' };
13
- <% }) %>
14
-
15
- const segments = {<% Object.values(t.schema.segments).filter((segment) => segment.emitSchema && segment.segmentType !== 'mixin').forEach((segment, i) => { %>
16
- '<%= segment.segmentName %>': schema<%= i %>,
17
- <% }) %>
18
- <% if(t.hasMixins) { %>
19
- ...mixins,
20
- <% } %>
21
- };
22
-
23
- const schema = {
24
- $schema: '<%- t.VovkSchemaIdEnum.SCHEMA %>',
25
- segments,
26
- meta,
27
- };
28
-
29
- export { schema };
@@ -1,3 +0,0 @@
1
- import type { DiffResult } from './diffSegmentSchema.mjs';
2
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
- export declare function logDiffResult(segmentName: string, diffResult: DiffResult, projectInfo: ProjectInfo): void;
@@ -1,3 +0,0 @@
1
- import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- import type { Segment } from '../utils/locateSegments.mjs';
3
- export declare function ensureClient(projectInfo: ProjectInfo, locatedSegments: Segment[]): Promise<void>;