vovk-cli 0.0.1-draft.11 → 0.0.1-draft.112

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 (90) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +29 -1
  3. package/client-templates/main/main.cjs.ejs +14 -0
  4. package/client-templates/main/main.d.cts.ejs +14 -0
  5. package/client-templates/module/module.d.mts.ejs +14 -0
  6. package/client-templates/module/module.mjs.ejs +20 -0
  7. package/client-templates/python/__init__.py +276 -0
  8. package/client-templates/ts/index.ts.ejs +23 -0
  9. package/dist/dev/diffSegmentSchema.d.mts +36 -0
  10. package/dist/{watcher/diffSchema.mjs → dev/diffSegmentSchema.mjs} +4 -12
  11. package/dist/{watcher → dev}/ensureSchemaFiles.d.mts +3 -0
  12. package/dist/{watcher → dev}/ensureSchemaFiles.mjs +53 -26
  13. package/dist/dev/index.d.mts +6 -0
  14. package/dist/{watcher → dev}/index.mjs +150 -77
  15. package/dist/dev/isSegmentSchemaEmpty.d.mts +2 -0
  16. package/dist/dev/isSegmentSchemaEmpty.mjs +4 -0
  17. package/dist/dev/logDiffResult.d.mts +3 -0
  18. package/dist/dev/logDiffResult.mjs +57 -0
  19. package/dist/dev/writeConfigJson.d.mts +2 -0
  20. package/dist/dev/writeConfigJson.mjs +15 -0
  21. package/dist/dev/writeOneSegmentSchemaFile.d.mts +12 -0
  22. package/dist/{watcher/writeOneSchemaFile.mjs → dev/writeOneSegmentSchemaFile.mjs} +11 -7
  23. package/dist/generate/ensureClient.d.mts +4 -0
  24. package/dist/generate/ensureClient.mjs +31 -0
  25. package/dist/generate/getClientTemplates.d.mts +16 -0
  26. package/dist/generate/getClientTemplates.mjs +42 -0
  27. package/dist/generate/index.d.mts +13 -0
  28. package/dist/generate/index.mjs +105 -0
  29. package/dist/getProjectInfo/getConfig.d.mts +3 -3
  30. package/dist/getProjectInfo/getConfig.mjs +22 -5
  31. package/dist/getProjectInfo/getConfigAbsolutePaths.mjs +2 -2
  32. package/dist/getProjectInfo/getRelativeSrcRoot.mjs +1 -1
  33. package/dist/getProjectInfo/getUserConfig.d.mts +1 -1
  34. package/dist/getProjectInfo/getUserConfig.mjs +3 -1
  35. package/dist/getProjectInfo/importUncachedModule.mjs +0 -1
  36. package/dist/getProjectInfo/importUncachedModuleWorker.mjs +0 -1
  37. package/dist/getProjectInfo/index.d.mts +14 -5
  38. package/dist/getProjectInfo/index.mjs +21 -13
  39. package/dist/index.d.mts +0 -28
  40. package/dist/index.mjs +60 -64
  41. package/dist/init/checkTSConfigForExperimentalDecorators.mjs +2 -2
  42. package/dist/init/createConfig.d.mts +3 -4
  43. package/dist/init/createConfig.mjs +12 -10
  44. package/dist/init/getTemplateFilesFromPackage.d.mts +2 -1
  45. package/dist/init/getTemplateFilesFromPackage.mjs +4 -5
  46. package/dist/init/index.d.mts +2 -3
  47. package/dist/init/index.mjs +31 -88
  48. package/dist/init/installDependencies.d.mts +1 -1
  49. package/dist/init/installDependencies.mjs +1 -1
  50. package/dist/init/logUpdateDependenciesError.d.mts +2 -2
  51. package/dist/init/logUpdateDependenciesError.mjs +3 -3
  52. package/dist/init/updateDependenciesWithoutInstalling.d.mts +3 -2
  53. package/dist/init/updateDependenciesWithoutInstalling.mjs +7 -9
  54. package/dist/init/updateNPMScripts.d.mts +3 -1
  55. package/dist/init/updateNPMScripts.mjs +13 -7
  56. package/dist/init/updateTypeScriptConfig.mjs +2 -2
  57. package/dist/initProgram.d.mts +2 -0
  58. package/dist/initProgram.mjs +21 -0
  59. package/dist/locateSegments.d.mts +7 -1
  60. package/dist/locateSegments.mjs +9 -6
  61. package/dist/new/addClassToSegmentCode.d.mts +1 -2
  62. package/dist/new/addClassToSegmentCode.mjs +5 -5
  63. package/dist/new/addCommonTerms.mjs +1 -0
  64. package/dist/new/index.d.mts +2 -2
  65. package/dist/new/index.mjs +3 -2
  66. package/dist/new/newModule.d.mts +3 -2
  67. package/dist/new/newModule.mjs +39 -34
  68. package/dist/new/newSegment.mjs +8 -6
  69. package/dist/new/render.d.mts +5 -2
  70. package/dist/new/render.mjs +25 -13
  71. package/dist/postinstall.mjs +16 -19
  72. package/dist/types.d.mts +35 -40
  73. package/dist/utils/debounceWithArgs.d.mts +2 -2
  74. package/dist/utils/debounceWithArgs.mjs +24 -9
  75. package/dist/utils/formatLoggedSegmentName.mjs +1 -1
  76. package/dist/utils/getAvailablePort.mjs +2 -1
  77. package/dist/utils/getFileSystemEntryType.mjs +1 -1
  78. package/package.json +23 -18
  79. package/templates/controller.ejs +22 -23
  80. package/templates/service.ejs +13 -13
  81. package/dist/generateClient.d.mts +0 -7
  82. package/dist/generateClient.mjs +0 -97
  83. package/dist/watcher/diffSchema.d.mts +0 -43
  84. package/dist/watcher/index.d.mts +0 -6
  85. package/dist/watcher/isMetadataEmpty.d.mts +0 -2
  86. package/dist/watcher/isMetadataEmpty.mjs +0 -4
  87. package/dist/watcher/logDiffResult.d.mts +0 -3
  88. package/dist/watcher/logDiffResult.mjs +0 -90
  89. package/dist/watcher/writeOneSchemaFile.d.mts +0 -11
  90. package/templates/worker.ejs +0 -24
@@ -0,0 +1,105 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import ejs from 'ejs';
4
+ import matter from 'gray-matter';
5
+ import uniq from 'lodash/uniq.js';
6
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
7
+ import prettify from '../utils/prettify.mjs';
8
+ import getClientTemplates from './getClientTemplates.mjs';
9
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
10
+ export default async function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }) {
11
+ const segmentsSchema = fullSchema.segments;
12
+ const generateFrom = templates ?? projectInfo.config.generateFrom;
13
+ const noClient = templates?.[0] === 'none';
14
+ const { config, cwd, log, clientImports, apiRoot } = projectInfo;
15
+ const { clientOutDirAbsolutePath, templateFiles } = getClientTemplates({ config, cwd, generateFrom });
16
+ // Ensure that each segment has a matching schema if it needs to be emitted:
17
+ for (let i = 0; i < segments.length; i++) {
18
+ const { segmentName } = segments[i];
19
+ const schema = segmentsSchema[segmentName];
20
+ if (!schema) {
21
+ throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
22
+ }
23
+ if (!schema.emitSchema)
24
+ continue;
25
+ }
26
+ const now = Date.now();
27
+ // Data for the EJS templates:
28
+ const t = {
29
+ apiRoot,
30
+ imports: clientImports,
31
+ fullSchema,
32
+ segmentMeta: Object.fromEntries(segments.map(({ segmentName, ...s }) => [segmentName, s])),
33
+ };
34
+ // Process each template in parallel
35
+ const processedTemplates = noClient
36
+ ? []
37
+ : await Promise.all(templateFiles.map(async ({ templatePath, outPath, templateName }) => {
38
+ /*const parsed = matter((await ejs.render(codeTemplate, { t }, { async: true, filename: templateFileName })).trim());
39
+ const { dir, fileName, sourceName, compiledName } = parsed.data as VovkModuleRenderResult;
40
+ const code = empty ? (sourceName ? `export default class ${sourceName} {}` : '') : parsed.content;*/
41
+ // Read the EJS template
42
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
43
+ const { data, content } = matter(templateContent);
44
+ if (data.imports instanceof Array) {
45
+ for (const imp of data.imports) {
46
+ t.imports = {
47
+ ...t.imports,
48
+ [imp]: await import(imp),
49
+ };
50
+ }
51
+ }
52
+ // Render the template
53
+ let rendered = templatePath.endsWith('.ejs')
54
+ ? ejs.render(content, { t }, {
55
+ filename: templatePath,
56
+ })
57
+ : templateContent;
58
+ // Optionally prettify
59
+ if (prettifyClient || config.prettifyClient) {
60
+ rendered = await prettify(rendered, outPath);
61
+ }
62
+ // Read existing file content to compare
63
+ const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
64
+ // Determine if we need to rewrite the file, ignore 1st line
65
+ const needsWriting = existingContent.trim().split('\n').slice(1).join('\n') !== rendered.trim().split('\n').slice(1).join('\n');
66
+ return {
67
+ outPath,
68
+ rendered,
69
+ needsWriting,
70
+ templateName,
71
+ };
72
+ }));
73
+ const usedTemplateNames = uniq(processedTemplates.filter(({ needsWriting }) => needsWriting).map(({ templateName }) => templateName));
74
+ let fullSchemaNames = [];
75
+ const DEFAULT_NAME = 'full-schema.json';
76
+ if (emitFullSchema) {
77
+ fullSchemaNames.push(typeof fullSchema === 'string' ? fullSchema : DEFAULT_NAME);
78
+ fullSchemaNames.push(...templateFiles
79
+ .filter(({ emitFullSchema }) => emitFullSchema)
80
+ .map(({ emitFullSchema }) => (typeof emitFullSchema === 'string' ? emitFullSchema : DEFAULT_NAME)));
81
+ }
82
+ fullSchemaNames = uniq(fullSchemaNames);
83
+ if (fullSchemaNames.length) {
84
+ await Promise.all(fullSchemaNames.map(async (name) => {
85
+ const fullSchemaOutAbsolutePath = path.resolve(clientOutDirAbsolutePath, name);
86
+ await fs.writeFile(fullSchemaOutAbsolutePath, JSON.stringify(segmentsSchema, null, 2));
87
+ log.info(`Full schema has ben written to ${fullSchemaOutAbsolutePath}`);
88
+ }));
89
+ }
90
+ if (usedTemplateNames.length === 0) {
91
+ const logOrDebug = forceNothingWrittenLog ? log.info : log.debug;
92
+ logOrDebug(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
93
+ return { written: false, path: clientOutDirAbsolutePath };
94
+ }
95
+ // Write updated files where needed
96
+ await Promise.all(processedTemplates.map(async ({ outPath, rendered, needsWriting }) => {
97
+ if (needsWriting) {
98
+ await fs.mkdir(clientOutDirAbsolutePath, { recursive: true });
99
+ return fs.writeFile(outPath, rendered);
100
+ }
101
+ return null;
102
+ }));
103
+ log.info(`Client generated from template${usedTemplateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(usedTemplateNames.map((s) => `"${s}"`).join(', '))} at ${clientOutDirAbsolutePath} in ${Date.now() - now}ms`);
104
+ return { written: true, path: clientOutDirAbsolutePath };
105
+ }
@@ -1,11 +1,11 @@
1
- import type { VovkConfig } from '../types.mjs';
1
+ import type { VovkStrictConfig } from 'vovk';
2
2
  export default function getConfig({ clientOutDir, cwd }: {
3
3
  clientOutDir?: string;
4
4
  cwd: string;
5
5
  }): Promise<{
6
- config: Required<VovkConfig>;
6
+ config: VovkStrictConfig;
7
7
  srcRoot: string;
8
8
  configAbsolutePaths: string[];
9
- userConfig: VovkConfig | null;
9
+ userConfig: import("vovk").VovkConfig | null;
10
10
  error: Error | undefined;
11
11
  }>;
@@ -5,25 +5,42 @@ export default async function getConfig({ clientOutDir, cwd }) {
5
5
  const { configAbsolutePaths, error, userConfig } = await getUserConfig({ cwd });
6
6
  const conf = userConfig ?? {};
7
7
  const srcRoot = await getRelativeSrcRoot({ cwd });
8
+ const validateOnClientImport = env.VOVK_VALIDATE_ON_CLIENT_PATH ?? conf.validateOnClientImport ?? null;
9
+ const fetcherImport = env.VOVK_FETCHER_PATH ?? conf.fetcherImport ?? 'vovk';
10
+ const createRPCImport = env.VOVK_CREATE_RPC_PATH ?? conf.createRPCImport ?? 'vovk';
11
+ const defaultClientTemplates = ['ts', 'module', 'main'];
8
12
  const config = {
13
+ emitConfig: [],
9
14
  modulesDir: env.VOVK_MODULES_DIR ?? conf.modulesDir ?? './' + [srcRoot, 'modules'].filter(Boolean).join('/'),
10
- validateOnClient: env.VOVK_VALIDATE_ON_CLIENT ?? conf.validateOnClient ?? null,
11
- validationLibrary: env.VOVK_VALIDATION_LIBRARY ?? conf.validationLibrary ?? null,
12
- fetcher: env.VOVK_FETCHER ?? conf.fetcher ?? 'vovk/client/defaultFetcher',
15
+ validateOnClientImport: typeof validateOnClientImport === 'string' ? [validateOnClientImport] : validateOnClientImport,
16
+ fetcherImport: typeof fetcherImport === 'string' ? [fetcherImport] : fetcherImport,
17
+ createRPCImport: typeof createRPCImport === 'string' ? [createRPCImport] : createRPCImport,
13
18
  schemaOutDir: env.VOVK_SCHEMA_OUT_DIR ?? conf.schemaOutDir ?? './.vovk-schema',
14
19
  clientOutDir: clientOutDir ?? env.VOVK_CLIENT_OUT_DIR ?? conf.clientOutDir ?? './node_modules/.vovk-client',
15
20
  origin: (env.VOVK_ORIGIN ?? conf.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
16
21
  rootEntry: env.VOVK_ROOT_ENTRY ?? conf.rootEntry ?? 'api',
17
22
  rootSegmentModulesDirName: env.VOVK_ROOT_SEGMENT_MODULES_DIR_NAME ?? conf.rootSegmentModulesDirName ?? '',
18
- logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'debug', // TODO: change to 'warn' when v3 is ready
23
+ logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'info',
19
24
  prettifyClient: (env.VOVK_PRETTIFY_CLIENT ? !!env.VOVK_PRETTIFY_CLIENT : null) ?? conf.prettifyClient ?? false,
20
25
  devHttps: (env.VOVK_DEV_HTTPS ? !!env.VOVK_DEV_HTTPS : null) ?? conf.devHttps ?? false,
26
+ generateFrom: typeof conf.generateFrom === 'function'
27
+ ? conf.generateFrom(defaultClientTemplates)
28
+ : (conf.generateFrom ?? ['ts', 'module', 'main']),
21
29
  templates: {
22
30
  service: 'vovk-cli/templates/service.ejs',
23
31
  controller: 'vovk-cli/templates/controller.ejs',
24
- worker: 'vovk-cli/templates/worker.ejs',
25
32
  ...conf.templates,
26
33
  },
34
+ custom: conf.custom ?? {},
27
35
  };
36
+ if (typeof conf.emitConfig === 'undefined') {
37
+ config.emitConfig = ['custom'];
38
+ }
39
+ else if (conf.emitConfig === true) {
40
+ config.emitConfig = Object.keys(config);
41
+ }
42
+ else if (Array.isArray(conf.emitConfig)) {
43
+ config.emitConfig = conf.emitConfig;
44
+ } // else it's false and emitConfig already is []
28
45
  return { config, srcRoot, configAbsolutePaths, userConfig, error };
29
46
  }
@@ -1,5 +1,5 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  export default async function getConfigAbsolutePaths({ cwd, relativePath, }) {
4
4
  const rootDir = path.resolve(cwd, relativePath || '');
5
5
  const baseName = 'vovk.config';
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import getFileSystemEntryType, { FileSystemEntryType } from '../utils/getFileSystemEntryType.mjs';
3
3
  export default async function getRelativeSrcRoot({ cwd }) {
4
4
  // Next.js Docs: src/app or src/pages will be ignored if app or pages are present in the root directory.
@@ -1,4 +1,4 @@
1
- import type { VovkConfig } from '../types.mjs';
1
+ import type { VovkConfig } from 'vovk';
2
2
  declare function getUserConfig({ cwd, }: {
3
3
  cwd: string;
4
4
  }): Promise<{
@@ -1,3 +1,4 @@
1
+ import { pathToFileURL } from 'node:url';
1
2
  import getConfigAbsolutePaths from './getConfigAbsolutePaths.mjs';
2
3
  import importUncachedModule from './importUncachedModule.mjs';
3
4
  async function getUserConfig({ cwd, }) {
@@ -14,7 +15,8 @@ async function getUserConfig({ cwd, }) {
14
15
  catch {
15
16
  try {
16
17
  const cacheBuster = Date.now();
17
- ({ default: userConfig } = (await import(`${configPath}?cache=${cacheBuster}`)));
18
+ const configPathUrl = pathToFileURL(configPath).href;
19
+ ({ default: userConfig } = (await import(`${configPathUrl}?cache=${cacheBuster}`)));
18
20
  }
19
21
  catch (e) {
20
22
  return { userConfig: null, configAbsolutePaths, error: e };
@@ -3,7 +3,6 @@ import { Worker } from 'node:worker_threads';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import './importUncachedModuleWorker.mjs'; // required for TS compilation
6
- // TODO comments
7
6
  function importUncachedModule(modulePath) {
8
7
  return new Promise((resolve, reject) => {
9
8
  const __filename = fileURLToPath(import.meta.url);
@@ -1,7 +1,6 @@
1
1
  import { parentPort, workerData as wData } from 'node:worker_threads';
2
2
  import { pathToFileURL } from 'node:url';
3
3
  void (async () => {
4
- // TODO Comments
5
4
  if (!parentPort)
6
5
  return;
7
6
  const workerData = wData;
@@ -6,13 +6,22 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, cwd, }?:
6
6
  }): Promise<{
7
7
  cwd: string;
8
8
  port: string;
9
- apiEntryPoint: string;
9
+ apiRoot: string;
10
10
  apiDir: string;
11
11
  srcRoot: string;
12
- schemaOutImportPath: string;
13
- fetcherClientImportPath: string;
14
- validateOnClientImportPath: string | null;
15
- config: Required<import("../types.mjs").VovkConfig>;
12
+ config: import("vovk").VovkStrictConfig;
13
+ clientImports: {
14
+ fullSchema: string;
15
+ fetcher: string;
16
+ createRPC: string;
17
+ validateOnClient: string | null;
18
+ module: {
19
+ fullSchema: string;
20
+ fetcher: string;
21
+ createRPC: string;
22
+ validateOnClient: string | null;
23
+ };
24
+ };
16
25
  log: {
17
26
  info: (msg: string) => void;
18
27
  warn: (msg: string) => void;
@@ -1,4 +1,4 @@
1
- import path from 'path';
1
+ import path from 'node:path';
2
2
  import getConfig from './getConfig.mjs';
3
3
  import getLogger from '../utils/getLogger.mjs';
4
4
  export default async function getProjectInfo({ port: givenPort, clientOutDir, cwd = process.cwd(), } = {}) {
@@ -6,15 +6,10 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, cw
6
6
  // Make PORT available to the config file at getConfig
7
7
  process.env.PORT = port;
8
8
  const { config, srcRoot, configAbsolutePaths, userConfig, error } = await getConfig({ clientOutDir, cwd });
9
- const apiEntryPoint = `${config.origin ?? ''}/${config.rootEntry}`;
9
+ const apiRoot = `${config.origin ?? ''}/${config.rootEntry}`;
10
10
  const apiDir = path.join(srcRoot, 'app', config.rootEntry);
11
- const schemaOutImportPath = path.relative(config.clientOutDir, config.schemaOutDir);
12
- const fetcherClientImportPath = config.fetcher.startsWith('.')
13
- ? path.relative(config.clientOutDir, config.fetcher)
14
- : config.fetcher;
15
- const validateOnClientImportPath = config.validateOnClient?.startsWith('.')
16
- ? path.relative(config.clientOutDir, config.validateOnClient)
17
- : config.validateOnClient;
11
+ const schemaOutImportPath = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/') + // windows fix
12
+ '/main.cjs';
18
13
  const log = getLogger(config.logLevel);
19
14
  if (configAbsolutePaths.length > 1) {
20
15
  log.warn(`Multiple config files found. Using the first one: ${configAbsolutePaths[0]}`);
@@ -22,16 +17,29 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, cw
22
17
  if (!userConfig && configAbsolutePaths.length > 0) {
23
18
  log.error(`Error reading config file at ${configAbsolutePaths[0]}: ${error?.message ?? 'Unknown Error'}`);
24
19
  }
20
+ const getImportPath = (p) => (p.startsWith('.') ? path.relative(config.clientOutDir, p) : p);
21
+ const clientImports = {
22
+ fullSchema: schemaOutImportPath,
23
+ fetcher: getImportPath(config.fetcherImport[0]),
24
+ createRPC: getImportPath(config.createRPCImport[0]),
25
+ validateOnClient: config.validateOnClientImport ? getImportPath(config.validateOnClientImport[0]) : null,
26
+ module: {
27
+ fullSchema: schemaOutImportPath,
28
+ fetcher: getImportPath(config.fetcherImport[1] ?? config.fetcherImport[0]),
29
+ createRPC: getImportPath(config.createRPCImport[1] ?? config.createRPCImport[0]),
30
+ validateOnClient: config.validateOnClientImport
31
+ ? getImportPath(config.validateOnClientImport[1] ?? config.validateOnClientImport[0])
32
+ : null,
33
+ },
34
+ };
25
35
  return {
26
36
  cwd,
27
37
  port,
28
- apiEntryPoint,
38
+ apiRoot,
29
39
  apiDir,
30
40
  srcRoot,
31
- schemaOutImportPath,
32
- fetcherClientImportPath,
33
- validateOnClientImportPath,
34
41
  config,
42
+ clientImports,
35
43
  log,
36
44
  };
37
45
  }
package/dist/index.d.mts CHANGED
@@ -1,30 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { Command } from 'commander';
3
- import type { LogLevelNames } from 'loglevel';
4
- import type { VovkConfig, VovkEnv } from './types.mjs';
5
2
  import 'dotenv/config';
6
- export type { VovkConfig, VovkEnv };
7
- export interface InitOptions {
8
- yes?: boolean;
9
- logLevel: LogLevelNames;
10
- useNpm?: boolean;
11
- useYarn?: boolean;
12
- usePnpm?: boolean;
13
- useBun?: boolean;
14
- skipInstall?: boolean;
15
- updateTsConfig?: boolean;
16
- updateScripts?: 'implicit' | 'explicit';
17
- validationLibrary?: string | null;
18
- validateOnClient?: boolean;
19
- dryRun?: boolean;
20
- channel?: 'latest' | 'beta' | 'dev';
21
- }
22
- export interface NewOptions {
23
- dryRun?: boolean;
24
- template?: string;
25
- dir?: string;
26
- overwrite?: boolean;
27
- noSegmentUpdate?: boolean;
28
- }
29
- declare const program: Command;
30
- export declare function initProgram(p: typeof program, command: string): Command;
package/dist/index.mjs CHANGED
@@ -1,51 +1,62 @@
1
1
  #!/usr/bin/env node
2
- import path from 'path';
2
+ import path from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { pathToFileURL } from 'node:url';
5
+ import 'dotenv/config';
3
6
  import { Command } from 'commander';
4
- import { readFileSync } from 'fs';
5
7
  import concurrently from 'concurrently';
6
8
  import getAvailablePort from './utils/getAvailablePort.mjs';
7
9
  import getProjectInfo from './getProjectInfo/index.mjs';
8
- import generateClient from './generateClient.mjs';
10
+ import generate from './generate/index.mjs';
9
11
  import locateSegments from './locateSegments.mjs';
10
- import { VovkCLIWatcher } from './watcher/index.mjs';
11
- import { Init } from './init/index.mjs';
12
+ import { VovkDev } from './dev/index.mjs';
12
13
  import newComponents from './new/index.mjs';
13
- import 'dotenv/config';
14
+ import initProgram from './initProgram.mjs';
14
15
  const program = new Command();
15
16
  const packageJSON = JSON.parse(readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf-8'));
16
17
  program.name('vovk').description('Vovk CLI').version(packageJSON.version);
18
+ initProgram(program.command('init'));
17
19
  program
18
20
  .command('dev')
19
- .description('Start schema watcher (optional flag --next-dev to start it with Next.js)')
20
- .option('--client-out <path>', 'Path to client output directory')
21
- .option('--next-dev', 'Start schema watcher and Next.js with automatic port allocation', false)
22
- .allowUnknownOption(true)
23
- .action(async (options, command) => {
21
+ .alias('d')
22
+ .description('start schema watcher (optional flag --next-dev to start it with Next.js)')
23
+ .argument('[nextArgs...]', 'extra arguments for the dev command')
24
+ .option('--next-dev', 'start schema watcher and Next.js with automatic port allocation')
25
+ .option('--exit', 'kill the processe when schema and client is generated')
26
+ .action(async (nextArgs, options) => {
27
+ const { nextDev, exit = false } = options;
24
28
  const portAttempts = 30;
25
- const PORT = !options.nextDev
29
+ const PORT = !nextDev
26
30
  ? process.env.PORT
27
31
  : process.env.PORT ||
28
- (await getAvailablePort(3000, portAttempts, 0, (failedPort, tryingPort) => console.warn(`🐺 Next.js Port ${failedPort} is in use, trying ${tryingPort} instead.`)).catch(() => {
29
- throw new Error(`🐺 ❌ Failed to find available Next port after ${portAttempts} attempts`);
32
+ (await getAvailablePort(3000, portAttempts, 0, (failedPort, tryingPort) =>
33
+ // eslint-disable-next-line no-console
34
+ console.warn(`🐺 Port ${failedPort} is in use, trying ${tryingPort} instead.`)).catch(() => {
35
+ throw new Error(`🐺 ❌ Failed to find an available port after ${portAttempts} attempts`);
30
36
  }));
31
37
  if (!PORT) {
32
38
  throw new Error('🐺 ❌ PORT env variable is required');
33
39
  }
34
- if (options.nextDev) {
40
+ if (nextDev) {
35
41
  const { result } = concurrently([
36
42
  {
37
- command: `node ${import.meta.dirname}/watcher/index.mjs`,
38
- name: 'Vovk.ts Schema Watcher',
39
- env: Object.assign({ PORT, __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true' }, options.clientOut ? { VOVK_CLIENT_OUT_DIR: options.clientOut } : {}),
40
- },
41
- {
42
- command: `npx next dev ${command.args.join(' ')}`,
43
+ command: `npx next dev ${nextArgs.join(' ')}`,
43
44
  name: 'Next.js Development Server',
44
45
  env: { PORT },
45
46
  },
47
+ {
48
+ command: `node ${import.meta.dirname}/dev/index.mjs`,
49
+ name: 'Vovk Dev Watcher',
50
+ env: {
51
+ PORT,
52
+ __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true',
53
+ __VOVK_EXIT__: exit ? 'true' : 'false',
54
+ },
55
+ },
46
56
  ], {
47
57
  killOthers: ['failure', 'success'],
48
58
  prefix: 'none',
59
+ successCondition: 'first',
49
60
  });
50
61
  try {
51
62
  await result;
@@ -55,65 +66,50 @@ program
55
66
  }
56
67
  }
57
68
  else {
58
- void new VovkCLIWatcher().start({ clientOutDir: options.clientOut });
69
+ void new VovkDev().start({ exit });
59
70
  }
60
71
  });
61
72
  program
62
73
  .command('generate')
63
- .description('Generate client')
64
- .option('--client-out <path>', 'Path to output directory')
74
+ .alias('g')
75
+ .description('Generate RPC client from schema')
76
+ .option('--out, --client-out-dir <path>', 'path to output directory')
77
+ .option('--template, --templates <templates...>', 'client code templates ("ts", "compiled", "python", "none", a custom path)')
78
+ .option('--emit-full-schema, --full-schema [fileName]', 'generate client with full schema')
79
+ .option('--prettify', 'prettify output files')
65
80
  .action(async (options) => {
66
- const projectInfo = await getProjectInfo({ clientOutDir: options.clientOut });
81
+ const { clientOutDir, templates, prettify, emitFullSchema } = options;
82
+ const projectInfo = await getProjectInfo({ clientOutDir });
67
83
  const { cwd, config, apiDir } = projectInfo;
68
- const segments = await locateSegments(apiDir);
84
+ const segments = await locateSegments({ dir: apiDir, config });
69
85
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
70
- const schema = (await import(path.join(schemaOutAbsolutePath, 'index.js')));
71
- await generateClient(projectInfo, segments, schema.default);
86
+ const schemaImportUrl = pathToFileURL(path.join(schemaOutAbsolutePath, 'main.cjs')).href;
87
+ const { default: fullSchema } = (await import(schemaImportUrl));
88
+ await generate({
89
+ projectInfo,
90
+ segments,
91
+ fullSchema,
92
+ templates,
93
+ prettify,
94
+ forceNothingWrittenLog: true,
95
+ emitFullSchema,
96
+ });
72
97
  });
73
- // reused at vovk-init
74
- export function initProgram(p, command) {
75
- return p
76
- .command(command + '[prefix]')
77
- .description('Initialize Vovk project')
78
- .option('-Y, --yes', 'Skip all prompts and use default values')
79
- .option('--log-level <level>', 'Set log level', 'info')
80
- .option('--use-npm', 'Use npm as package manager')
81
- .option('--use-yarn', 'Use yarn as package manager')
82
- .option('--use-pnpm', 'Use pnpm as package manager')
83
- .option('--use-bun', 'Use bun as package manager')
84
- .option('--skip-install', 'Skip installing dependencies')
85
- .option('--update-ts-config', 'Update tsconfig.json')
86
- .option('--update-scripts <mode>', 'Update package.json scripts (implicit or explicit)')
87
- .option('--validation-library <library>', 'Validation library to use ("vovk-zod", "vovk-yup", "vovk-dto" or another). Set to "none" to skip validation')
88
- .option('--validate-on-client', 'Validate on client')
89
- .option('--channel <channel>', 'Channel to use for fetching packages', 'latest')
90
- .option('--dry-run', 'Do not write files to disk')
91
- .action((prefix = '.', options) => new Init().main(prefix, options));
92
- }
93
- initProgram(program, 'init ');
94
98
  program
95
99
  .command('new [components...]')
96
100
  .alias('n')
97
- .description('Create new components. "vovk new [...components] [segmentName/]moduleName" to create a new module or "vovk new segment [segmentName]" to create a new segment')
98
- .option('-O, --overwrite', 'Overwrite existing files')
99
- .option('--template <template>', 'Override config template')
100
- .option('--dir <dirname>', 'Override dirName in template file. Relative to the root of the project')
101
- .option('--no-segment-update', 'Do not update segment files when creating a new module')
102
- .option('--dry-run', 'Do not write files to disk')
101
+ .description('create new components. "vovk new [...components] [segmentName/]moduleName" to create a new module or "vovk new segment [segmentName]" to create a new segment')
102
+ .option('-o, --overwrite', 'overwrite existing files')
103
+ .option('--template, --templates <templates...>', 'override config template; accepts an array of strings that correspond the order of the components')
104
+ .option('--dir <dirname>', 'override dirName in template file; relative to the root of the project')
105
+ .option('--empty', 'create an empty module')
106
+ .option('--no-segment-update', 'do not update segment files when creating a new module')
107
+ .option('--dry-run', 'do not write files to disk')
103
108
  .action((components, options) => newComponents(components, options));
104
109
  program
105
110
  .command('help')
106
111
  .description('Show help message')
107
112
  .action(() => program.help());
108
- /*
109
- TODO
110
- vovk new segment [segmentName]
111
- vovk new controller service [segmentName/]moduleName
112
- vovk new c s w [segmentName/]moduleName
113
-
114
- vovk c s w userApi/user
115
- vovk new c s w user
116
- */
117
113
  program.parse(process.argv);
118
114
  if (!process.argv.slice(2).length) {
119
115
  program.outputHelp();
@@ -1,5 +1,5 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
3
  import * as jsonc from 'jsonc-parser';
4
4
  export default async function checkTSConfigForExperimentalDecorators(root) {
5
5
  const tsconfigPath = path.resolve(root, 'tsconfig.json');
@@ -1,10 +1,9 @@
1
1
  import type getLogger from '../utils/getLogger.mjs';
2
- import type { InitOptions } from '../index.mjs';
3
- export default function createConfig({ root, log, dryRun, options: { validationLibrary, validateOnClient }, }: {
2
+ import type { InitOptions } from '../types.mjs';
3
+ export default function createConfig({ root, log, options: { validationLibrary, reactQuery, channel, dryRun }, }: {
4
4
  root: string;
5
5
  log: ReturnType<typeof getLogger>;
6
- dryRun?: boolean;
7
- options: Pick<InitOptions, 'validationLibrary' | 'validateOnClient'>;
6
+ options: Pick<InitOptions, 'validationLibrary' | 'reactQuery' | 'channel' | 'dryRun'>;
8
7
  }): Promise<{
9
8
  configAbsolutePath: string;
10
9
  }>;
@@ -1,9 +1,9 @@
1
- import path from 'path';
2
- import fs from 'fs/promises';
3
- import getFileSystemEntryType, { FileSystemEntryType } from '../utils/getFileSystemEntryType.mjs';
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
4
3
  import getTemplateFilesFromPackage from './getTemplateFilesFromPackage.mjs';
5
4
  import prettify from '../utils/prettify.mjs';
6
- export default async function createConfig({ root, log, dryRun, options: { validationLibrary, validateOnClient }, }) {
5
+ import getFileSystemEntryType, { FileSystemEntryType } from '../utils/getFileSystemEntryType.mjs';
6
+ export default async function createConfig({ root, log, options: { validationLibrary, reactQuery, channel, dryRun }, }) {
7
7
  const config = {};
8
8
  const dotConfigPath = path.join(root, '.config');
9
9
  const dir = (await getFileSystemEntryType(dotConfigPath)) === FileSystemEntryType.DIRECTORY ? dotConfigPath : root;
@@ -14,21 +14,23 @@ export default async function createConfig({ root, log, dryRun, options: { valid
14
14
  const templates = {
15
15
  controller: 'vovk-cli/templates/controller.ejs',
16
16
  service: 'vovk-cli/templates/service.ejs',
17
- worker: 'vovk-cli/templates/worker.ejs',
18
17
  };
19
18
  if (validationLibrary) {
20
- config.validationLibrary = validationLibrary;
21
- if (validateOnClient) {
22
- config.validateOnClient = `${validationLibrary}/validateOnClient`;
23
- }
19
+ config.validateOnClientImport =
20
+ {
21
+ 'vovk-dto': `vovk-dto/validateOnClient.js`,
22
+ }[validationLibrary] ?? 'vovk-ajv';
24
23
  try {
25
- const validationTemplates = await getTemplateFilesFromPackage(validationLibrary);
24
+ const validationTemplates = await getTemplateFilesFromPackage(validationLibrary, channel);
26
25
  Object.assign(templates, validationTemplates);
27
26
  }
28
27
  catch (error) {
29
28
  log.warn(`Failed to fetch validation library templates: ${error.message}`);
30
29
  }
31
30
  }
31
+ if (reactQuery) {
32
+ config.createRPCImport = 'vovk-react-query';
33
+ }
32
34
  config.templates = templates;
33
35
  const configStr = await prettify(`/** @type {import('vovk-cli').VovkConfig} */
34
36
  const config = ${JSON.stringify(config, null, 2)};
@@ -1,6 +1,7 @@
1
+ import { InitOptions } from '../types.mjs';
1
2
  /**
2
3
  * Retrieves a list of files in the 'templates' folder of an NPM package.
3
4
  * @param packageName - The name of the NPM package.
4
5
  * @returns A promise that resolves to an array of file paths.
5
6
  */
6
- export default function getTemplatesFiles(packageName: string, channel?: string): Promise<Record<string, string>>;
7
+ export default function getTemplateFilesFromPackage(packageName: string, channel?: InitOptions['channel']): Promise<Record<string, string>>;
@@ -1,14 +1,14 @@
1
- import { createGunzip } from 'zlib';
1
+ import { Readable } from 'node:stream';
2
+ import { createGunzip } from 'node:zlib';
2
3
  import tar from 'tar-stream';
3
- import { Readable } from 'stream';
4
4
  import getNPMPackageMetadata from '../utils/getNPMPackageMetadata.mjs';
5
+ // Crereated with AI
5
6
  /**
6
7
  * Retrieves a list of files in the 'templates' folder of an NPM package.
7
8
  * @param packageName - The name of the NPM package.
8
9
  * @returns A promise that resolves to an array of file paths.
9
10
  */
10
- export default async function getTemplatesFiles(packageName, channel = 'beta' // TODO change to latest
11
- ) {
11
+ export default async function getTemplateFilesFromPackage(packageName, channel = 'latest') {
12
12
  const metadata = await getNPMPackageMetadata(packageName);
13
13
  const latestVersion = metadata['dist-tags'][channel];
14
14
  const tarballUrl = metadata.versions[latestVersion].dist.tarball;
@@ -37,7 +37,6 @@ function extractTemplatesFromTarball(tarballBuffer) {
37
37
  const files = [];
38
38
  extract.on('entry', (header, stream, next) => {
39
39
  const filePath = header.name;
40
- // TODO revisit comments
41
40
  // Check if the file is in the 'templates' folder
42
41
  if (filePath.startsWith('package/templates/')) {
43
42
  files.push(filePath.replace('package/', ''));