vovk-cli 0.0.1-beta.17 → 0.0.1-beta.19

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 (41) hide show
  1. package/dist/generateClient.mjs +30 -28
  2. package/dist/getProjectInfo/getConfig.d.mts +1 -1
  3. package/dist/getProjectInfo/getConfig.mjs +8 -0
  4. package/dist/getProjectInfo/index.d.mts +2 -1
  5. package/dist/getProjectInfo/index.mjs +4 -0
  6. package/dist/index.d.mts +4 -1
  7. package/dist/index.mjs +21 -8
  8. package/dist/init/installDependencies.mjs +5 -5
  9. package/dist/new/addClassToSegmentCode.d.mts +6 -0
  10. package/dist/new/addClassToSegmentCode.mjs +32 -0
  11. package/dist/{generate/addCommonTerms.js → new/addCommonTerms.mjs} +1 -0
  12. package/dist/new/index.d.mts +2 -0
  13. package/dist/new/index.mjs +24 -0
  14. package/dist/new/newModule.d.mts +5 -0
  15. package/dist/new/newModule.mjs +89 -0
  16. package/dist/new/newSegment.d.mts +4 -0
  17. package/dist/new/newSegment.mjs +33 -0
  18. package/dist/new/render.d.mts +12 -0
  19. package/dist/new/render.mjs +28 -0
  20. package/dist/types.d.mts +7 -0
  21. package/dist/utils/chalkHighlightThing.mjs +1 -1
  22. package/dist/utils/formatLoggedSegmentName.d.mts +4 -1
  23. package/dist/utils/formatLoggedSegmentName.mjs +4 -2
  24. package/dist/utils/prettify.d.mts +1 -0
  25. package/dist/utils/prettify.mjs +10 -0
  26. package/dist/watcher/ensureSchemaFiles.d.mts +1 -1
  27. package/dist/watcher/ensureSchemaFiles.mjs +16 -16
  28. package/dist/watcher/index.mjs +26 -28
  29. package/dist/watcher/logDiffResult.mjs +9 -9
  30. package/dist/watcher/writeOneSchemaFile.d.mts +2 -2
  31. package/dist/watcher/writeOneSchemaFile.mjs +2 -2
  32. package/package.json +7 -2
  33. package/templates/controller.ejs +85 -0
  34. package/templates/service.ejs +9 -0
  35. package/templates/worker.ejs +9 -0
  36. package/templates/zod/MyThingController.c.only.template.ts +32 -0
  37. package/templates/zod/MyThingController.c.template.ts +39 -0
  38. package/templates/zod/MyThingService.s.template.ts +18 -0
  39. package/dist/generate/replaceOccurrences.d.mts +0 -1
  40. package/dist/generate/replaceOccurrences.mjs +0 -49
  41. /package/dist/{generate/addCommonTerms.d.ts → new/addCommonTerms.d.mts} +0 -0
@@ -1,34 +1,33 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import formatLoggedSegmentName from './utils/formatLoggedSegmentName.mjs';
4
+ import prettify from './utils/prettify.mjs';
4
5
  export default async function generateClient(projectInfo, segments, segmentsSchema) {
6
+ const { config, cwd, log, validateOnClientImportPath, apiEntryPoint, fetcherClientImportPath, schemaOutImportPath } = projectInfo;
5
7
  const now = Date.now();
6
- const clientoOutDirFullPath = path.join(projectInfo.cwd, projectInfo.config.clientOutDir);
7
- const validateFullPath = projectInfo.config.validateOnClient?.startsWith('.')
8
- ? path.join(projectInfo.cwd, projectInfo.config.validateOnClient)
9
- : projectInfo.config.validateOnClient;
8
+ const clientoOutDirAbsolutePath = path.join(cwd, config.clientOutDir);
10
9
  let dts = `// auto-generated
11
10
  /* eslint-disable */
12
11
  import type { clientizeController } from 'vovk/client';
13
12
  import type { promisifyWorker } from 'vovk/worker';
14
13
  import type { VovkClientFetcher } from 'vovk/client';
15
- import type fetcher from '${projectInfo.fetcherClientImportPath}';
14
+ import type fetcher from '${fetcherClientImportPath}';
16
15
 
17
16
  `;
18
17
  let js = `// auto-generated
19
18
  /* eslint-disable */
20
19
  const { clientizeController } = require('vovk/client');
21
20
  const { promisifyWorker } = require('vovk/worker');
22
- const { default: fetcher } = require('${projectInfo.fetcherClientImportPath}');
23
- const schema = require('${projectInfo.schemaOutImportPath}');
21
+ const { default: fetcher } = require('${fetcherClientImportPath}');
22
+ const schema = require('${schemaOutImportPath}');
24
23
  `;
25
24
  let ts = `// auto-generated
26
25
  /* eslint-disable */
27
26
  import { clientizeController } from 'vovk/client';
28
27
  import { promisifyWorker } from 'vovk/worker';
29
28
  import type { VovkClientFetcher } from 'vovk/client';
30
- import fetcher from '${projectInfo.fetcherClientImportPath}';
31
- import schema from '${projectInfo.schemaOutImportPath}';
29
+ import fetcher from '${fetcherClientImportPath}';
30
+ import schema from '${schemaOutImportPath}';
32
31
 
33
32
  `;
34
33
  for (let i = 0; i < segments.length; i++) {
@@ -39,7 +38,7 @@ import schema from '${projectInfo.schemaOutImportPath}';
39
38
  }
40
39
  if (!schema.emitSchema)
41
40
  continue;
42
- const importRouteFilePath = path.relative(projectInfo.config.clientOutDir, routeFilePath);
41
+ const importRouteFilePath = path.relative(config.clientOutDir, routeFilePath);
43
42
  dts += `import type { Controllers as Controllers${i}, Workers as Workers${i} } from "${importRouteFilePath}";\n`;
44
43
  ts += `import type { Controllers as Controllers${i}, Workers as Workers${i} } from "${importRouteFilePath}";\n`;
45
44
  }
@@ -47,13 +46,13 @@ import schema from '${projectInfo.schemaOutImportPath}';
47
46
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
48
47
  `;
49
48
  ts += `
50
- ${validateFullPath ? `import validateOnClient from '${validateFullPath}';\n` : '\nconst validateOnClient = undefined;'}
49
+ ${validateOnClientImportPath ? `import validateOnClient from '${validateOnClientImportPath}';\n` : '\nconst validateOnClient = undefined;'}
51
50
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
52
- const prefix = '${projectInfo.apiEntryPoint}';
51
+ const prefix = '${apiEntryPoint}';
53
52
  `;
54
53
  js += `
55
- const { default: validateOnClient = null } = ${validateFullPath ? `require('${validateFullPath}')` : '{}'};
56
- const prefix = '${projectInfo.apiEntryPoint}';
54
+ const { default: validateOnClient = null } = ${validateOnClientImportPath ? `require('${validateOnClientImportPath}')` : '{}'};
55
+ const prefix = '${apiEntryPoint}';
57
56
  `;
58
57
  for (let i = 0; i < segments.length; i++) {
59
58
  const { segmentName } = segments[i];
@@ -74,20 +73,23 @@ const prefix = '${projectInfo.apiEntryPoint}';
74
73
  ts += `export const ${key} = promisifyWorker<Workers${i}["${key}"]>(null, schema['${segmentName}'].workers.${key});\n`;
75
74
  }
76
75
  }
77
- const localJsPath = path.join(clientoOutDirFullPath, 'client.js');
78
- const localDtsPath = path.join(clientoOutDirFullPath, 'client.d.ts');
79
- const localTsPath = path.join(clientoOutDirFullPath, 'index.ts');
80
- const existingJs = await fs.readFile(localJsPath, 'utf-8').catch(() => '');
81
- const existingDts = await fs.readFile(localDtsPath, 'utf-8').catch(() => '');
82
- const existingTs = await fs.readFile(localTsPath, 'utf-8').catch(() => '');
76
+ const localJsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'client.js');
77
+ const localDtsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'client.d.ts');
78
+ const localTsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'index.ts');
79
+ const existingJs = await fs.readFile(localJsAbsolutePath, 'utf-8').catch(() => '');
80
+ const existingDts = await fs.readFile(localDtsAbsolutePath, 'utf-8').catch(() => '');
81
+ const existingTs = await fs.readFile(localTsAbsolutePath, 'utf-8').catch(() => '');
82
+ js = await prettify(js, localJsAbsolutePath);
83
+ dts = await prettify(dts, localDtsAbsolutePath);
84
+ ts = await prettify(ts, localTsAbsolutePath);
83
85
  if (existingJs === js && existingDts === dts && existingTs === ts) {
84
- projectInfo.log.info(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
85
- return { written: false, path: clientoOutDirFullPath };
86
+ log.info(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
87
+ return { written: false, path: clientoOutDirAbsolutePath };
86
88
  }
87
- await fs.mkdir(clientoOutDirFullPath, { recursive: true });
88
- await fs.writeFile(localJsPath, js);
89
- await fs.writeFile(localDtsPath, dts);
90
- await fs.writeFile(localTsPath, ts);
91
- projectInfo.log.info(`Client generated in ${Date.now() - now}ms`);
92
- return { written: true, path: clientoOutDirFullPath };
89
+ await fs.mkdir(clientoOutDirAbsolutePath, { recursive: true });
90
+ await fs.writeFile(localJsAbsolutePath, js);
91
+ await fs.writeFile(localDtsAbsolutePath, dts);
92
+ await fs.writeFile(localTsAbsolutePath, ts);
93
+ log.info(`Client generated in ${Date.now() - now}ms`);
94
+ return { written: true, path: clientoOutDirAbsolutePath };
93
95
  }
@@ -2,6 +2,6 @@ import type { VovkConfig } from '../types.mjs';
2
2
  export default function getConfig({ clientOutDir }: {
3
3
  clientOutDir?: string;
4
4
  }): Promise<{
5
- config: Required<VovkConfig>;
5
+ config: Required<Omit<VovkConfig, "_devForceAppDir">>;
6
6
  srcRoot: string;
7
7
  }>;
@@ -15,6 +15,14 @@ export default async function getConfig({ clientOutDir }) {
15
15
  rootEntry: env.VOVK_ROOT_ENTRY ?? userConfig.rootEntry ?? 'api',
16
16
  rootSegmentModulesDirName: env.VOVK_ROOT_SEGMENT_MODULES_DIR_NAME ?? userConfig.rootSegmentModulesDirName ?? '',
17
17
  logLevel: env.VOVK_LOG_LEVEL ?? userConfig.logLevel ?? 'debug', // TODO: change to 'warn' when v3 is ready
18
+ custom: userConfig.custom ?? {},
19
+ templates: {
20
+ service: 'vovk-cli/templates/service.ejs',
21
+ controller: 'vovk-cli/templates/controller.ejs',
22
+ worker: 'vovk-cli/templates/worker.ejs',
23
+ ...userConfig.templates,
24
+ },
18
25
  };
26
+ // forceAppDir is used for testing purposes
19
27
  return { config, srcRoot };
20
28
  }
@@ -10,7 +10,8 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, }?: {
10
10
  srcRoot: string;
11
11
  schemaOutImportPath: string;
12
12
  fetcherClientImportPath: string;
13
- config: Required<import("../types.mjs").VovkConfig>;
13
+ validateOnClientImportPath: string | null;
14
+ config: Required<Omit<import("../types.mjs").VovkConfig, "_devForceAppDir">>;
14
15
  log: {
15
16
  info: (msg: string) => void;
16
17
  warn: (msg: string) => void;
@@ -13,6 +13,9 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, }
13
13
  const fetcherClientImportPath = config.fetcher.startsWith('.')
14
14
  ? path.relative(config.clientOutDir, config.fetcher)
15
15
  : config.fetcher;
16
+ const validateOnClientImportPath = config.validateOnClient?.startsWith('.')
17
+ ? path.relative(config.clientOutDir, config.validateOnClient)
18
+ : config.validateOnClient;
16
19
  const log = getLogger(config.logLevel);
17
20
  return {
18
21
  cwd,
@@ -22,6 +25,7 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, }
22
25
  srcRoot,
23
26
  schemaOutImportPath,
24
27
  fetcherClientImportPath,
28
+ validateOnClientImportPath,
25
29
  config,
26
30
  log,
27
31
  };
package/dist/index.d.mts CHANGED
@@ -1,8 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { VovkConfig, VovkEnv } from './types.mjs';
3
2
  import type { LogLevelNames } from 'loglevel';
3
+ import type { VovkConfig, VovkEnv } from './types.mjs';
4
4
  export type { VovkConfig, VovkEnv };
5
5
  export interface InitOptions {
6
6
  yes: boolean;
7
7
  logLevel: LogLevelNames;
8
8
  }
9
+ export interface NewOptions {
10
+ dryRun: boolean;
11
+ }
package/dist/index.mjs CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import path from 'path';
2
3
  import { Command } from 'commander';
4
+ import { readFileSync } from 'fs';
3
5
  import concurrently from 'concurrently';
4
6
  import getAvailablePort from './utils/getAvailablePort.mjs';
5
- import { VovkCLIWatcher } from './watcher/index.mjs';
6
7
  import getProjectInfo from './getProjectInfo/index.mjs';
7
8
  import generateClient from './generateClient.mjs';
8
9
  import locateSegments from './locateSegments.mjs';
9
- import path from 'path';
10
- import { readFileSync } from 'fs';
10
+ import { VovkCLIWatcher } from './watcher/index.mjs';
11
11
  import { Init } from './init/index.mjs';
12
+ import newComponents from './new/index.mjs';
12
13
  const program = new Command();
13
14
  const packageJSON = JSON.parse(readFileSync(path.join(import.meta.dirname, '../package.json'), 'utf-8'));
14
15
  program.name('vovk').description('Vovk CLI').version(packageJSON.version);
@@ -66,8 +67,8 @@ program
66
67
  const projectInfo = await getProjectInfo({ clientOutDir: options.clientOut });
67
68
  const { cwd, config, apiDir } = projectInfo;
68
69
  const segments = await locateSegments(apiDir);
69
- const schemaOutFullPath = path.join(cwd, config.schemaOutDir);
70
- const schema = (await import(path.join(schemaOutFullPath, 'index.js')));
70
+ const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
71
+ const schema = (await import(path.join(schemaOutAbsolutePath, 'index.js')));
71
72
  await generateClient(projectInfo, segments, schema.default);
72
73
  });
73
74
  program
@@ -75,13 +76,25 @@ program
75
76
  .description('Initialize Vovk project')
76
77
  .option('-Y, --yes', 'Skip all prompts and use default values')
77
78
  .option('--log-level <level>', 'Set log level', 'info')
78
- .action(async (prefix = '.', options) => {
79
- await Init.main(prefix, options);
80
- });
79
+ .action((prefix = '.', options) => Init.main(prefix, options));
80
+ program
81
+ .command('new [components...]')
82
+ .alias('n')
83
+ .description('Create new components. "vovk new [...components] [segmentName/]moduleName" to create a new module or "vovk new segment [segmentName]" to create a new segment')
84
+ .option('--dry-run', 'Do not write files to disk')
85
+ .action((components, options) => newComponents(components, options));
81
86
  program
82
87
  .command('help')
83
88
  .description('Show help message')
84
89
  .action(() => program.help());
90
+ /*
91
+ vovk new segment [segmentName]
92
+ vovk new controller service [segmentName/]moduleName
93
+ vovk new c s w [segmentName/]moduleName
94
+
95
+ vovk c s w userApi/user
96
+ vovk new c s w user
97
+ */
85
98
  program.parse(process.argv);
86
99
  if (!process.argv.slice(2).length) {
87
100
  program.outputHelp();
@@ -3,19 +3,19 @@ import { promisify } from 'util';
3
3
  import * as path from 'path';
4
4
  const execPromise = promisify(exec);
5
5
  export default async function installDependencies(installDir, dependencies, devDependencies) {
6
- const fullPath = path.resolve(installDir);
6
+ const absolutePath = path.resolve(installDir);
7
7
  try {
8
8
  if (dependencies.length > 0) {
9
- console.log(`Installing dependencies in ${fullPath}...`);
10
- const { stdout, stderr } = await execPromise(`npm install ${dependencies.join(' ')} --prefix ${fullPath}`);
9
+ console.log(`Installing dependencies in ${absolutePath}...`);
10
+ const { stdout, stderr } = await execPromise(`npm install ${dependencies.join(' ')} --prefix ${absolutePath}`);
11
11
  console.log(stdout);
12
12
  if (stderr) {
13
13
  console.error(stderr);
14
14
  }
15
15
  }
16
16
  if (devDependencies.length > 0) {
17
- console.log(`Installing dev dependencies in ${fullPath}...`);
18
- const { stdout, stderr } = await execPromise(`npm install --save-dev ${devDependencies.join(' ')} --prefix ${fullPath}`);
17
+ console.log(`Installing dev dependencies in ${absolutePath}...`);
18
+ const { stdout, stderr } = await execPromise(`npm install --save-dev ${devDependencies.join(' ')} --prefix ${absolutePath}`);
19
19
  console.log(stdout);
20
20
  if (stderr) {
21
21
  console.error(stderr);
@@ -0,0 +1,6 @@
1
+ export default function addClassToSegmentCode(segmentSourceCode: string, { className, rpcName, type, importPath, }: {
2
+ className: string;
3
+ rpcName: string;
4
+ type: 'worker' | 'controller';
5
+ importPath: string;
6
+ }): string;
@@ -0,0 +1,32 @@
1
+ import { Project, SyntaxKind } from 'ts-morph';
2
+ export default function addClassToSegmentCode(segmentSourceCode, { className, rpcName, type, importPath, }) {
3
+ const project = new Project();
4
+ const sourceFile = project.createSourceFile('route.ts', segmentSourceCode, { overwrite: true });
5
+ // Add the import if it doesn't exist
6
+ let importDeclaration = sourceFile.getImportDeclaration((imp) => {
7
+ return imp.getModuleSpecifierValue() === importPath;
8
+ });
9
+ if (!importDeclaration) {
10
+ importDeclaration = sourceFile.addImportDeclaration({
11
+ defaultImport: className,
12
+ moduleSpecifier: importPath,
13
+ });
14
+ }
15
+ // Get the variable declaration for controllers or workers
16
+ const variableDeclaration = sourceFile.getVariableDeclaration(`${type}s`);
17
+ if (variableDeclaration) {
18
+ const initializer = variableDeclaration.getInitializer();
19
+ if (initializer && initializer.getKind() === SyntaxKind.ObjectLiteralExpression) {
20
+ const objectLiteral = initializer.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
21
+ // Check if the property already exists
22
+ const existingProperty = objectLiteral.getProperty(rpcName);
23
+ if (!existingProperty) {
24
+ objectLiteral.addPropertyAssignment({
25
+ name: rpcName,
26
+ initializer: className,
27
+ });
28
+ }
29
+ }
30
+ }
31
+ return sourceFile.getFullText();
32
+ }
@@ -1,4 +1,5 @@
1
1
  import pluralize from 'pluralize';
2
+ // feel free to open a direct PR if you have more common terms to add
2
3
  const terms = [
3
4
  ['entity', 'entities'],
4
5
  ['regex', 'regexes'],
@@ -0,0 +1,2 @@
1
+ import type { NewOptions } from '../index.mjs';
2
+ export default function newComponents(components: string[], options: NewOptions): Promise<void>;
@@ -0,0 +1,24 @@
1
+ import newModule from './newModule.mjs';
2
+ import newSegment from './newSegment.mjs';
3
+ export default async function newComponents(components, options) {
4
+ if (components[0] === 'segment' || components[0] === 'segments') {
5
+ let segmentNames = components.slice(1);
6
+ if (!segmentNames.length) {
7
+ segmentNames = [''];
8
+ }
9
+ for (const segmentName of segmentNames) {
10
+ await newSegment({ segmentName, dryRun: options.dryRun });
11
+ }
12
+ }
13
+ else {
14
+ if (components.length < 2) {
15
+ throw new Error('Invalid command invocation. Please provide at least two arguments.');
16
+ }
17
+ const what = components.slice(0, -1);
18
+ const moduleNameWithOptionalSegment = components[components.length - 1];
19
+ if (!moduleNameWithOptionalSegment) {
20
+ throw new Error('A module name with an optional segment cannot be empty');
21
+ }
22
+ await newModule({ what, moduleNameWithOptionalSegment, dryRun: options.dryRun });
23
+ }
24
+ }
@@ -0,0 +1,5 @@
1
+ export default function newModule({ what, moduleNameWithOptionalSegment, dryRun, }: {
2
+ what: string[];
3
+ moduleNameWithOptionalSegment: string;
4
+ dryRun: boolean;
5
+ }): Promise<void>;
@@ -0,0 +1,89 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import getProjectInfo from '../getProjectInfo/index.mjs';
4
+ import render from './render.mjs';
5
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
6
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
7
+ import locateSegments from '../locateSegments.mjs';
8
+ import addClassToSegmentCode from './addClassToSegmentCode.mjs';
9
+ import fileExists from '../utils/fileExists.mjs';
10
+ import prettify from '../utils/prettify.mjs';
11
+ function splitByLast(str, delimiter = '/') {
12
+ const index = str.lastIndexOf(delimiter);
13
+ if (index === -1) {
14
+ // Delimiter not found; return empty string and the original string
15
+ return ['', str];
16
+ }
17
+ const before = str.substring(0, index);
18
+ const after = str.substring(index + delimiter.length);
19
+ return [before, after];
20
+ }
21
+ export default async function newModule({ what, moduleNameWithOptionalSegment, dryRun, }) {
22
+ const { config, log, apiDir, cwd } = await getProjectInfo();
23
+ const templates = config.templates;
24
+ const [segmentName, moduleName] = splitByLast(moduleNameWithOptionalSegment);
25
+ // replace c by controller, s by service, w by worker, everything else keeps the same
26
+ what = what.map((s) => {
27
+ switch (s) {
28
+ case 'c':
29
+ return 'controller';
30
+ case 's':
31
+ return 'service';
32
+ case 'w':
33
+ return 'worker';
34
+ default:
35
+ return s;
36
+ }
37
+ });
38
+ // check if template exists
39
+ for (const type of what) {
40
+ if (!templates[type]) {
41
+ throw new Error(`Template for ${type} not found in config`);
42
+ }
43
+ }
44
+ const segments = await locateSegments(apiDir);
45
+ const segment = segments.find((s) => s.segmentName === segmentName);
46
+ if (!segment) {
47
+ throw new Error(`Segment ${segmentName} not found`);
48
+ }
49
+ for (const type of what) {
50
+ const templatePath = templates[type];
51
+ const templateAbsolutePath = path.join(cwd, '../..', templatePath); // TODO WRONG, use import.meta.resolve, also in other modules for fetcher, validateOnClient, etc.
52
+ const templateCode = await fs.readFile(templateAbsolutePath, 'utf-8');
53
+ const { fileName, className, rpcName, code } = await render(templateCode, {
54
+ config,
55
+ withService: what.includes('service'),
56
+ segmentName,
57
+ moduleName,
58
+ });
59
+ console.log(config.modulesDir, fileName);
60
+ const absoluteModulePath = path.join(config.modulesDir, fileName);
61
+ const dirName = path.dirname(absoluteModulePath);
62
+ const prettiedCode = await prettify(code, absoluteModulePath);
63
+ if (!dryRun) {
64
+ if (await fileExists(absoluteModulePath)) {
65
+ log.warn(`File ${chalkHighlightThing(absoluteModulePath)} already exists, skipping this "${type}"`);
66
+ }
67
+ else {
68
+ await fs.mkdir(dirName, { recursive: true });
69
+ await fs.writeFile(absoluteModulePath, prettiedCode);
70
+ }
71
+ }
72
+ if (type === 'controller' || type === 'worker') {
73
+ const { routeFilePath } = segment;
74
+ const segmentSourceCode = await fs.readFile(routeFilePath, 'utf-8');
75
+ const importPath = path.relative(dirName, fileName); // TODO WRONG
76
+ const newSegmentCode = addClassToSegmentCode(segmentSourceCode, {
77
+ className,
78
+ rpcName,
79
+ type,
80
+ importPath,
81
+ });
82
+ if (!dryRun) {
83
+ await fs.writeFile(routeFilePath, newSegmentCode);
84
+ }
85
+ log.info(`Added ${chalkHighlightThing(className)} ${type} to ${formatLoggedSegmentName(segmentName)} as ${chalkHighlightThing(rpcName)}`);
86
+ }
87
+ log.info(`Created ${chalkHighlightThing(fileName)} with ${chalkHighlightThing(type)} template for ${formatLoggedSegmentName(segmentName)}`);
88
+ }
89
+ }
@@ -0,0 +1,4 @@
1
+ export default function newSegment({ segmentName, dryRun }: {
2
+ segmentName: string;
3
+ dryRun: boolean;
4
+ }): Promise<void>;
@@ -0,0 +1,33 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ import getProjectInfo from '../getProjectInfo/index.mjs';
4
+ import fileExists from '../utils/fileExists.mjs';
5
+ import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
6
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
7
+ import prettify from '../utils/prettify.mjs';
8
+ export default async function newSegment({ segmentName, dryRun }) {
9
+ const { apiDir, cwd, log } = await getProjectInfo();
10
+ const absoluteSegmentRoutePath = path.join(cwd, apiDir, segmentName, '[[...vovk]]/route.ts');
11
+ if (await fileExists(absoluteSegmentRoutePath)) {
12
+ return log.error(`Unable to create new segment. ${formatLoggedSegmentName(segmentName, { upperFirst: true })} already exists.`);
13
+ }
14
+ const code = await prettify(`import { initVovk } from 'vovk';
15
+
16
+ const controllers = {};
17
+ const workers = {};
18
+
19
+ export type Controllers = typeof controllers;
20
+ export type Workers = typeof workers;
21
+
22
+ export const { GET, POST, PATCH, PUT, HEAD, OPTIONS, DELETE } = initVovk({
23
+ emitSchema: true,
24
+ workers,
25
+ controllers,
26
+ });
27
+ `, absoluteSegmentRoutePath);
28
+ if (!dryRun) {
29
+ await fs.mkdir(path.dirname(absoluteSegmentRoutePath), { recursive: true });
30
+ await fs.writeFile(absoluteSegmentRoutePath, code);
31
+ }
32
+ log.info(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} created at ${absoluteSegmentRoutePath}. Run ${chalkHighlightThing(`vovk new controller ${segmentName}/someName`)} to create a controller or modify the segment file manually`);
33
+ }
@@ -0,0 +1,12 @@
1
+ import type { VovkConfig } from '../types.mjs';
2
+ export default function render(codeTemplate: string, { config, withService, segmentName, moduleName, }: {
3
+ config: VovkConfig;
4
+ withService: boolean;
5
+ segmentName: string;
6
+ moduleName: string;
7
+ }): Promise<{
8
+ fileName: string;
9
+ className: string;
10
+ rpcName: string;
11
+ code: string;
12
+ }>;
@@ -0,0 +1,28 @@
1
+ import ejs from 'ejs';
2
+ import matter from 'gray-matter';
3
+ import _ from 'lodash';
4
+ import pluralize from 'pluralize';
5
+ import addCommonTerms from './addCommonTerms.mjs';
6
+ addCommonTerms();
7
+ export default async function render(codeTemplate, { config, withService, segmentName, moduleName, }) {
8
+ const getFileDir = (givenSegmentName, givenModuleName) => [_.camelCase(givenModuleName), givenSegmentName || config.rootSegmentModulesDirName].filter(Boolean).join('/') +
9
+ '/';
10
+ const templateVars = {
11
+ // input
12
+ config,
13
+ withService,
14
+ segmentName,
15
+ moduleName,
16
+ // utils
17
+ getFileDir,
18
+ // libraries
19
+ _, // lodash
20
+ pluralize,
21
+ };
22
+ // first, render the front matter because it can use ejs variables
23
+ const parsed = matter((await ejs.render(codeTemplate, templateVars, { async: true })).trim());
24
+ const { fileName, className, rpcName } = parsed.data;
25
+ const templateContent = parsed.content;
26
+ const code = await ejs.render(templateContent, templateVars, { async: true });
27
+ return { fileName, className, rpcName, code };
28
+ }
package/dist/types.d.mts CHANGED
@@ -26,4 +26,11 @@ export type VovkConfig = {
26
26
  origin?: string;
27
27
  rootSegmentModulesDirName?: string;
28
28
  logLevel?: LogLevelNames;
29
+ custom?: KnownAny;
30
+ templates?: {
31
+ service?: string;
32
+ controller?: string;
33
+ worker?: string;
34
+ [key: string]: string | undefined;
35
+ };
29
36
  };
@@ -1,4 +1,4 @@
1
1
  import chalk from 'chalk';
2
2
  export default function chalkHighlightThing(str) {
3
- return chalk.cyan.bold(str);
3
+ return chalk.whiteBright.bold(str);
4
4
  }
@@ -1 +1,4 @@
1
- export default function formatLoggedSegmentName(segmentName: string, withChalk?: boolean): string;
1
+ export default function formatLoggedSegmentName(segmentName: string, { withChalk, upperFirst }?: {
2
+ withChalk?: boolean;
3
+ upperFirst?: boolean;
4
+ }): string;
@@ -1,5 +1,7 @@
1
1
  import chalkHighlightThing from './chalkHighlightThing.mjs';
2
- export default function formatLoggedSegmentName(segmentName, withChalk = false) {
3
- const text = segmentName ? `segment "${segmentName}"` : 'the root segment';
2
+ import upperFirstLodash from 'lodash/upperFirst.js';
3
+ export default function formatLoggedSegmentName(segmentName, { withChalk = true, upperFirst = false } = {}) {
4
+ let text = segmentName ? `segment "${segmentName}"` : 'the root segment';
5
+ text = upperFirst ? upperFirstLodash(text) : text;
4
6
  return withChalk ? chalkHighlightThing(text) : text;
5
7
  }
@@ -0,0 +1 @@
1
+ export default function prettify(code: string, absoluteFilePath: string): Promise<string>;
@@ -0,0 +1,10 @@
1
+ import prettier from 'prettier';
2
+ export default async function prettify(code, absoluteFilePath) {
3
+ const options = await prettier.resolveConfig(absoluteFilePath);
4
+ const finalOptions = {
5
+ ...options,
6
+ filepath: absoluteFilePath, // for selecting the correct parser
7
+ };
8
+ const formattedCode = await prettier.format(code, finalOptions);
9
+ return formattedCode;
10
+ }
@@ -1,3 +1,3 @@
1
1
  import { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- export default function ensureSchemaFiles(schemaOutFullPath: string, segmentNames: string[], projectInfo: ProjectInfo | null): Promise<void>;
2
+ export default function ensureSchemaFiles(projectInfo: ProjectInfo | null, schemaOutAbsolutePath: string, segmentNames: string[]): Promise<void>;
3
3
  export declare const debouncedEnsureSchemaFiles: import("lodash").DebouncedFunc<typeof ensureSchemaFiles>;
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import debounce from 'lodash/debounce.js';
4
4
  import writeOneSchemaFile, { ROOT_SEGMENT_SCHEMA_NAME } from './writeOneSchemaFile.mjs';
5
5
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
6
- export default async function ensureSchemaFiles(schemaOutFullPath, segmentNames, projectInfo) {
6
+ export default async function ensureSchemaFiles(projectInfo, schemaOutAbsolutePath, segmentNames) {
7
7
  const now = Date.now();
8
8
  let hasChanged = false;
9
9
  // Create index.js file
@@ -15,13 +15,13 @@ export default async function ensureSchemaFiles(schemaOutFullPath, segmentNames,
15
15
  const dTsContent = `import type { VovkSchema } from 'vovk';
16
16
  declare const segmentSchema: Record<string, VovkSchema>;
17
17
  export default segmentSchema;`;
18
- await fs.mkdir(schemaOutFullPath, { recursive: true });
19
- await fs.writeFile(path.join(schemaOutFullPath, 'index.js'), indexContent);
20
- await fs.writeFile(path.join(schemaOutFullPath, 'index.d.ts'), dTsContent);
21
- // Create JSON files (if not exist) with name [segmentName].json (where segmentName can include /, which means the folder structure can be nested) : {} (empty object)
18
+ await fs.mkdir(schemaOutAbsolutePath, { recursive: true });
19
+ await fs.writeFile(path.join(schemaOutAbsolutePath, 'index.js'), indexContent);
20
+ await fs.writeFile(path.join(schemaOutAbsolutePath, 'index.d.ts'), dTsContent);
21
+ // Create JSON files (if not exist) with name [segmentName].json (where segmentName can include /, which means the folder structure can be nested)
22
22
  await Promise.all(segmentNames.map(async (segmentName) => {
23
23
  const { isCreated } = await writeOneSchemaFile({
24
- schemaOutFullPath,
24
+ schemaOutAbsolutePath,
25
25
  schema: {
26
26
  emitSchema: false,
27
27
  segmentName,
@@ -31,7 +31,7 @@ export default segmentSchema;`;
31
31
  skipIfExists: true,
32
32
  });
33
33
  if (isCreated) {
34
- projectInfo?.log.debug(`Created empty schema file for ${formatLoggedSegmentName(segmentName, true)}`);
34
+ projectInfo?.log.debug(`Created empty schema file for ${formatLoggedSegmentName(segmentName)}`);
35
35
  hasChanged = true;
36
36
  }
37
37
  }));
@@ -39,32 +39,32 @@ export default segmentSchema;`;
39
39
  async function deleteUnnecessaryJsonFiles(dirPath) {
40
40
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
41
41
  await Promise.all(entries.map(async (entry) => {
42
- const fullPath = path.join(dirPath, entry.name);
42
+ const absolutePath = path.join(dirPath, entry.name);
43
43
  if (entry.isDirectory()) {
44
44
  // Recursively delete unnecessary files and folders within nested directories
45
- await deleteUnnecessaryJsonFiles(fullPath);
45
+ await deleteUnnecessaryJsonFiles(absolutePath);
46
46
  // Check if the directory is empty after deletion and remove it if so
47
- const remainingEntries = await fs.readdir(fullPath);
47
+ const remainingEntries = await fs.readdir(absolutePath);
48
48
  if (remainingEntries.length === 0) {
49
- await fs.rmdir(fullPath);
50
- projectInfo?.log.debug(`Deleted unnecessary schema directory "${fullPath}"`);
49
+ await fs.rmdir(absolutePath);
50
+ projectInfo?.log.debug(`Deleted unnecessary schema directory "${absolutePath}"`);
51
51
  hasChanged = true;
52
52
  }
53
53
  }
54
54
  else if (entry.isFile() && entry.name.endsWith('.json')) {
55
- const relativePath = path.relative(schemaOutFullPath, fullPath);
55
+ const relativePath = path.relative(schemaOutAbsolutePath, absolutePath);
56
56
  const segmentName = relativePath.replace(/\\/g, '/').slice(0, -5); // Remove '.json' extension
57
57
  if (!segmentNames.includes(segmentName) &&
58
58
  !segmentNames.includes(segmentName.replace(ROOT_SEGMENT_SCHEMA_NAME, ''))) {
59
- await fs.unlink(fullPath);
60
- projectInfo?.log.debug(`Deleted unnecessary schema file for ${formatLoggedSegmentName(segmentName, true)}`);
59
+ await fs.unlink(absolutePath);
60
+ projectInfo?.log.debug(`Deleted unnecessary schema file for ${formatLoggedSegmentName(segmentName)}`);
61
61
  hasChanged = true;
62
62
  }
63
63
  }
64
64
  }));
65
65
  }
66
66
  // Start the recursive deletion from the root directory
67
- await deleteUnnecessaryJsonFiles(schemaOutFullPath);
67
+ await deleteUnnecessaryJsonFiles(schemaOutAbsolutePath);
68
68
  if (hasChanged)
69
69
  projectInfo?.log.info(`Schema files updated in ${Date.now() - now}ms`);
70
70
  }
@@ -12,7 +12,7 @@ import debounce from 'lodash/debounce.js';
12
12
  import isEmpty from 'lodash/isEmpty.js';
13
13
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
14
14
  import keyBy from 'lodash/keyBy.js';
15
- import { capitalize } from 'lodash';
15
+ import capitalize from 'lodash/capitalize.js';
16
16
  export class VovkCLIWatcher {
17
17
  #projectInfo;
18
18
  #segments = [];
@@ -23,12 +23,12 @@ export class VovkCLIWatcher {
23
23
  #watchSegments = () => {
24
24
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
25
25
  const { cwd, log, config, apiDir } = this.#projectInfo;
26
- const schemaOutFullPath = path.join(cwd, config.schemaOutDir);
27
- const apiDirFullPath = path.join(cwd, apiDir);
28
- const getSegmentName = (filePath) => path.relative(apiDirFullPath, filePath).replace(segmentReg, '');
29
- log.debug(`Watching segments in ${apiDirFullPath}`);
26
+ const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
27
+ const apiDirAbsolutePath = path.join(cwd, apiDir);
28
+ const getSegmentName = (filePath) => path.relative(apiDirAbsolutePath, filePath).replace(segmentReg, '');
29
+ log.debug(`Watching segments in ${apiDirAbsolutePath}`);
30
30
  this.#segmentWatcher = chokidar
31
- .watch(apiDirFullPath, {
31
+ .watch(apiDirAbsolutePath, {
32
32
  persistent: true,
33
33
  ignoreInitial: true,
34
34
  })
@@ -39,10 +39,9 @@ export class VovkCLIWatcher {
39
39
  this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
40
40
  ? this.#segments
41
41
  : [...this.#segments, { routeFilePath: filePath, segmentName }];
42
- log.info(`${capitalize(formatLoggedSegmentName(segmentName, true))} has been added`);
42
+ log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
43
43
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
44
- void debouncedEnsureSchemaFiles(schemaOutFullPath, this.#segments.map((s) => s.segmentName), this.#projectInfo // TODO refactor
45
- );
44
+ void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
46
45
  }
47
46
  })
48
47
  .on('change', (filePath) => {
@@ -54,7 +53,7 @@ export class VovkCLIWatcher {
54
53
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
55
54
  .on('addDir', async (dirPath) => {
56
55
  log.debug(`Directory ${dirPath} has been added to segments folder`);
57
- this.#segments = await locateSegments(apiDirFullPath);
56
+ this.#segments = await locateSegments(apiDirAbsolutePath);
58
57
  for (const { segmentName } of this.#segments) {
59
58
  void this.#requestSchema(segmentName);
60
59
  }
@@ -62,7 +61,7 @@ export class VovkCLIWatcher {
62
61
  // eslint-disable-next-line @typescript-eslint/no-misused-promises
63
62
  .on('unlinkDir', async (dirPath) => {
64
63
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
65
- this.#segments = await locateSegments(apiDirFullPath);
64
+ this.#segments = await locateSegments(apiDirAbsolutePath);
66
65
  for (const { segmentName } of this.#segments) {
67
66
  void this.#requestSchema(segmentName);
68
67
  }
@@ -72,10 +71,9 @@ export class VovkCLIWatcher {
72
71
  if (segmentReg.test(filePath)) {
73
72
  const segmentName = getSegmentName(filePath);
74
73
  this.#segments = this.#segments.filter((s) => s.segmentName !== segmentName);
75
- log.info(`${capitalize(formatLoggedSegmentName(segmentName, true))} has been removed`);
74
+ log.info(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} has been removed`);
76
75
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
77
- void debouncedEnsureSchemaFiles(schemaOutFullPath, this.#segments.map((s) => s.segmentName), this.#projectInfo // TODO refactor
78
- );
76
+ void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
79
77
  }
80
78
  })
81
79
  .on('ready', () => {
@@ -87,10 +85,10 @@ export class VovkCLIWatcher {
87
85
  };
88
86
  #watchModules = () => {
89
87
  const { config, cwd, log } = this.#projectInfo;
90
- const modulesDirFullPath = path.join(cwd, config.modulesDir);
91
- log.debug(`Watching modules in ${modulesDirFullPath}`);
88
+ const modulesDirAbsolutePath = path.join(cwd, config.modulesDir);
89
+ log.debug(`Watching modules in ${modulesDirAbsolutePath}`);
92
90
  this.#modulesWatcher = chokidar
93
- .watch(modulesDirFullPath, {
91
+ .watch(modulesDirAbsolutePath, {
94
92
  persistent: true,
95
93
  ignoreInitial: true,
96
94
  })
@@ -193,10 +191,10 @@ export class VovkCLIWatcher {
193
191
  #requestSchema = debounceWithArgs(async (segmentName) => {
194
192
  const { apiEntryPoint, log, port } = this.#projectInfo;
195
193
  const endpoint = `${apiEntryPoint.startsWith('http') ? apiEntryPoint : `http://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
196
- log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName, true)} at ${endpoint}`);
194
+ log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
197
195
  const resp = await fetch(endpoint);
198
196
  if (resp.status !== 200) {
199
- log.warn(`Schema request to ${formatLoggedSegmentName(segmentName, true)} failed with status code ${resp.status}. Expected 200.`);
197
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status}. Expected 200.`);
200
198
  return;
201
199
  }
202
200
  let schema = null;
@@ -204,7 +202,7 @@ export class VovkCLIWatcher {
204
202
  ({ schema } = (await resp.json()));
205
203
  }
206
204
  catch (error) {
207
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName, true)}: ${error.message}`);
205
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
208
206
  }
209
207
  await this.#handleSchema(schema);
210
208
  }, 500);
@@ -215,7 +213,7 @@ export class VovkCLIWatcher {
215
213
  log.warn('Segment schema is null');
216
214
  return;
217
215
  }
218
- const schemaOutFullPath = path.join(cwd, config.schemaOutDir);
216
+ const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
219
217
  const segment = this.#segments.find((s) => s.segmentName === schema.segmentName);
220
218
  if (!segment) {
221
219
  log.warn(`Segment "${schema.segmentName}" not found`);
@@ -225,18 +223,18 @@ export class VovkCLIWatcher {
225
223
  if (schema.emitSchema) {
226
224
  const now = Date.now();
227
225
  const { diffResult } = await writeOneSchemaFile({
228
- schemaOutFullPath,
226
+ schemaOutAbsolutePath,
229
227
  schema,
230
228
  skipIfExists: false,
231
229
  });
232
230
  const timeTook = Date.now() - now;
233
231
  if (diffResult) {
234
232
  logDiffResult(segment.segmentName, diffResult, this.#projectInfo);
235
- log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName, true)} has been updated in ${timeTook}ms`);
233
+ log.info(`Schema for ${formatLoggedSegmentName(segment.segmentName)} has been updated in ${timeTook}ms`);
236
234
  }
237
235
  }
238
236
  else if (schema && (!isEmpty(schema.controllers) || !isEmpty(schema.workers))) {
239
- log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName, true)} but emitSchema is false`);
237
+ log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but emitSchema is false`);
240
238
  }
241
239
  if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
242
240
  log.debug(`All segments with "emitSchema" have schema.`);
@@ -252,10 +250,10 @@ export class VovkCLIWatcher {
252
250
  process.on('unhandledRejection', (reason) => {
253
251
  log.error(`Unhandled Rejection: ${String(reason)}`);
254
252
  });
255
- const apiDirFullPath = path.join(cwd, apiDir);
256
- const schemaOutFullPath = path.join(cwd, config.schemaOutDir);
257
- this.#segments = await locateSegments(apiDirFullPath);
258
- await debouncedEnsureSchemaFiles(schemaOutFullPath, this.#segments.map((s) => s.segmentName), this.#projectInfo);
253
+ const apiDirAbsolutePath = path.join(cwd, apiDir);
254
+ const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
255
+ this.#segments = await locateSegments(apiDirAbsolutePath);
256
+ await debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
259
257
  // Request schema every segment in 3 seconds in order to update schema and start watching
260
258
  setTimeout(() => {
261
259
  for (const { segmentName } of this.#segments) {
@@ -31,43 +31,43 @@ export default function logDiffResult(segmentName, diffResult, projectInfo) {
31
31
  case 'worker':
32
32
  switch (diffNormalizedItem.type) {
33
33
  case 'added':
34
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
34
+ projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName)}`);
35
35
  break;
36
36
  case 'removed':
37
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
37
+ projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName)}`);
38
38
  break;
39
39
  }
40
40
  break;
41
41
  case 'controller':
42
42
  switch (diffNormalizedItem.type) {
43
43
  case 'added':
44
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
44
+ projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName)}`);
45
45
  break;
46
46
  case 'removed':
47
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
47
+ projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName)}`);
48
48
  break;
49
49
  }
50
50
  break;
51
51
  case 'workerHandler':
52
52
  switch (diffNormalizedItem.type) {
53
53
  case 'added':
54
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
54
+ projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName)}`);
55
55
  break;
56
56
  case 'removed':
57
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
57
+ projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName)}`);
58
58
  break;
59
59
  }
60
60
  break;
61
61
  case 'controllerHandler':
62
62
  switch (diffNormalizedItem.type) {
63
63
  case 'added':
64
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName, true)}`);
64
+ projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been added at ${formatLoggedSegmentName(segmentName)}`);
65
65
  break;
66
66
  case 'removed':
67
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName, true)}`);
67
+ projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been removed from ${formatLoggedSegmentName(segmentName)}`);
68
68
  break;
69
69
  case 'changed':
70
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been changed at ${formatLoggedSegmentName(segmentName, true)}`);
70
+ projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been changed at ${formatLoggedSegmentName(segmentName)}`);
71
71
  break;
72
72
  }
73
73
  break;
@@ -1,8 +1,8 @@
1
1
  import type { VovkSchema } from 'vovk';
2
2
  import { DiffResult } from './diffSchema.mjs';
3
3
  export declare const ROOT_SEGMENT_SCHEMA_NAME = "_root";
4
- export default function writeOneSchemaFile({ schemaOutFullPath, schema, skipIfExists, }: {
5
- schemaOutFullPath: string;
4
+ export default function writeOneSchemaFile({ schemaOutAbsolutePath, schema, skipIfExists, }: {
5
+ schemaOutAbsolutePath: string;
6
6
  schema: VovkSchema;
7
7
  skipIfExists?: boolean;
8
8
  }): Promise<{
@@ -2,8 +2,8 @@ import path from 'path';
2
2
  import fs from 'fs/promises';
3
3
  import diffSchema from './diffSchema.mjs';
4
4
  export const ROOT_SEGMENT_SCHEMA_NAME = '_root';
5
- export default async function writeOneSchemaFile({ schemaOutFullPath, schema, skipIfExists = false, }) {
6
- const segmentPath = path.join(schemaOutFullPath, `${schema.segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json`);
5
+ export default async function writeOneSchemaFile({ schemaOutAbsolutePath, schema, skipIfExists = false, }) {
6
+ const segmentPath = path.join(schemaOutAbsolutePath, `${schema.segmentName || ROOT_SEGMENT_SCHEMA_NAME}.json`);
7
7
  if (skipIfExists) {
8
8
  try {
9
9
  await fs.stat(segmentPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-beta.17",
3
+ "version": "0.0.1-beta.19",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -40,13 +40,18 @@
40
40
  "chokidar": "^3.6.0",
41
41
  "commander": "^12.1.0",
42
42
  "concurrently": "^8.2.2",
43
+ "ejs": "^3.1.10",
44
+ "gray-matter": "^4.0.3",
43
45
  "inflection": "^3.0.0",
44
46
  "jsonc-parser": "^3.3.1",
45
47
  "lodash": "^4.17.21",
46
48
  "loglevel": "^1.9.2",
47
- "pluralize": "^8.0.0"
49
+ "pluralize": "^8.0.0",
50
+ "prettier": "^3.3.3",
51
+ "ts-morph": "^23.0.0"
48
52
  },
49
53
  "devDependencies": {
54
+ "@types/ejs": "^3.1.5",
50
55
  "@types/npmcli__package-json": "^4.0.4",
51
56
  "@types/pluralize": "^0.0.33",
52
57
  "type-fest": "^4.26.0"
@@ -0,0 +1,85 @@
1
+ <% var modulePascalName = _.upperFirst(_.camelCase(moduleName)); %>
2
+ <% var modulePascalNamePlural = pluralize(modulePascalName); %>
3
+ <% var ControllerName = modulePascalName + 'Controller'; %>
4
+ <% var RPCName = modulePascalName + 'RPC'; %>
5
+ <% var ServiceName = modulePascalName + 'Service'; %>
6
+ ---
7
+ # Relative to modules dir
8
+ fileName: <%= getFileDir(segmentName, moduleName) + ControllerName + '.ts' %>
9
+ className: <%= ControllerName %> # Used to define a controller in a segment
10
+ rpcName: <%= RPCName %> # Used to define an exported RPC class in a segment
11
+ ---
12
+
13
+ import { prefix, get, put, post, del<%= !config.validationLibrary ? ', type VovkRequest' : '' %> } from 'vovk';
14
+ <% if(withService) { %>
15
+ import <%= ServiceName %> from './<%= ServiceName %>';
16
+ <% } %>
17
+ <% if(config.validationLibrary === 'vovk-zod') { %>
18
+ import { withZod } from 'vovk-zod';
19
+ import { z } from 'zod';
20
+ <% } %>
21
+
22
+ @prefix('<%= _.kebabCase(moduleName).toLowerCase() %>')
23
+ export default class <%= ControllerName %> {
24
+ @get()
25
+ <% if(config.validationLibrary === 'vovk-zod') { %>
26
+ async get<%= modulePascalNamePlural %> = withZod(null, z.object({ q: z.string() }), (req) => {
27
+ const q = req.nextUrl.searchParams.get('q');
28
+ <% if(withService) { %>
29
+ return <%= ServiceName %>.getMyThingsExample(q);
30
+ <% } else { %>
31
+ return { q };
32
+ <% } %>
33
+ });
34
+ <% } else { %>
35
+ static get<%= modulePascalNamePlural %> = async (req: VovkRequest<null, { q: string }>) => {
36
+ const q = req.nextUrl.searchParams.get('q');
37
+ <% if(withService) { %>
38
+ return <%= ServiceName %>.getMyThingsExample(q);
39
+ <% } else { %>
40
+ return { q };
41
+ <% } %>
42
+ }
43
+ <% } %>
44
+
45
+ @put(':id')
46
+ <% if(config.validationLibrary === 'vovk-zod') { %>
47
+ static update<%= modulePascalNamePlural %> = withZod(
48
+ z.object({
49
+ foo: z.union([z.literal('bar'), z.literal('baz')]),
50
+ }),
51
+ z.object({ q: z.string() }),
52
+ async (req, params: { id: string }) => {
53
+ const { id } = params;
54
+ const body = await req.json();
55
+ const q = req.nextUrl.searchParams.get('q');
56
+ <% if(withService) { %>
57
+ return MyThingService.updateMyThingExample(id, q, body);
58
+ <% } else { %>
59
+ return { id, body, q };
60
+ <% } %>
61
+ }
62
+ );
63
+ <% } else { %>
64
+ static update<%= modulePascalNamePlural %> = async (req: VovkRequest<{ foo: 'bar' | 'baz' }, { q: string }>, params: { id: string }) => {
65
+ const { id } = params;
66
+ const body = await req.json();
67
+ const q = req.nextUrl.searchParams.get('q');
68
+ <% if(withService) { %>
69
+ return MyThingService.updateMyThingExample(id, q, body);
70
+ <% } else { %>
71
+ return { id, body, q };
72
+ <% } %>
73
+ };
74
+ <% } %>
75
+
76
+ @post()
77
+ static create<%= modulePascalNamePlural %> = () => {
78
+ // ...
79
+ };
80
+
81
+ @del(':id')
82
+ static delete<%= modulePascalNamePlural %> = () => {
83
+ // ...
84
+ };
85
+ }
@@ -0,0 +1,9 @@
1
+ <% var modulePascalName = _.upperFirst(_.camelCase(moduleName); %>
2
+ <% var ServiceName = modulePascalName + 'Service'; %>
3
+
4
+ ---
5
+ # Relative to modules dir
6
+ fileName: <%= getFileDir(segmentName, moduleName) + ServiceName + '.ts' %>
7
+ ---
8
+
9
+ // TO DO: Implement <%= ServiceName %>
@@ -0,0 +1,9 @@
1
+ <% var modulePascalName = _.upperFirst(_.camelCase(moduleName); %>
2
+ <% var WorkerName = modulePascalName + 'Worker'; %>
3
+
4
+ ---
5
+ # Relative to modules dir
6
+ fileName: <%= getFileDir(segmentName, moduleName) + WorkerName + '.ts' %>
7
+ ---
8
+
9
+ // TO DO: Implement <%= WorkerName %>
@@ -0,0 +1,32 @@
1
+ import { prefix, get, put, post, del, type VovkRequest } from 'vovk';
2
+
3
+ @prefix('my-thing')
4
+ export default class MyThingController {
5
+ @get()
6
+ static getMyThingsExample = (req: VovkRequest<null, { q: string }>) => {
7
+ const q = req.nextUrl.searchParams.get('q');
8
+ return { q };
9
+ };
10
+
11
+ @put(':id')
12
+ static updateMyThingExample = async (
13
+ req: VovkRequest<{ foo: 'bar' | 'baz' }, { q: string }>,
14
+ params: { id: string }
15
+ ) => {
16
+ const { id } = params;
17
+ const body = await req.json();
18
+ const q = req.nextUrl.searchParams.get('q');
19
+
20
+ return { id, q, body };
21
+ };
22
+
23
+ @post()
24
+ static createMyThingExample = () => {
25
+ // ...
26
+ };
27
+
28
+ @del(':id')
29
+ static deleteMyThingExample = () => {
30
+ // ...
31
+ };
32
+ }
@@ -0,0 +1,39 @@
1
+ import { prefix, get, put, post, del } from 'vovk';
2
+ import { z } from 'zod';
3
+ import { withZod } from 'vovk-zod';
4
+ import MyThingService from './MyThingService.s.template';
5
+
6
+ @prefix('my-thing')
7
+ export default class MyThingController {
8
+ @get()
9
+ static getMyThingsExample = withZod(null, z.object({ q: z.string() }), (req) => {
10
+ const q = req.nextUrl.searchParams.get('q');
11
+
12
+ return MyThingService.getMyThingsExample(q);
13
+ });
14
+
15
+ @put(':id')
16
+ static updateMyThingExample = withZod(
17
+ z.object({
18
+ foo: z.union([z.literal('bar'), z.literal('baz')]),
19
+ }),
20
+ z.object({ q: z.string() }),
21
+ async (req, params: { id: string }) => {
22
+ const { id } = params;
23
+ const body = await req.json();
24
+ const q = req.nextUrl.searchParams.get('q');
25
+
26
+ return MyThingService.updateMyThingExample(id, q, body);
27
+ }
28
+ );
29
+
30
+ @post()
31
+ static async createMyThingExample() {
32
+ // ...
33
+ }
34
+
35
+ @del(':id')
36
+ static deleteMyThingExample() {
37
+ // ...
38
+ }
39
+ }
@@ -0,0 +1,18 @@
1
+ import type { VovkControllerBody, VovkControllerQuery } from 'vovk';
2
+ import type MyThingController from './MyThingController.c.template';
3
+
4
+ export default class MyThingService {
5
+ static getMyThingsExample = (q: VovkControllerQuery<typeof MyThingController.getMyThingsExample>['q']) => {
6
+ return { q };
7
+ };
8
+
9
+ static updateMyThingExample = (
10
+ id: string,
11
+ q: VovkControllerQuery<typeof MyThingController.updateMyThingExample>['q'],
12
+ body: VovkControllerBody<typeof MyThingController.updateMyThingExample>
13
+ ) => {
14
+ return { id, q, body };
15
+ };
16
+
17
+ // ...
18
+ }
@@ -1 +0,0 @@
1
- export default function replaceOccurrences(code: string, replacementSingular: string): string;
@@ -1,49 +0,0 @@
1
- import camelCase from 'lodash/camelCase.js';
2
- import kebabCase from 'lodash/kebabCase.js';
3
- import snakeCase from 'lodash/snakeCase.js';
4
- import upperFirst from 'lodash/upperFirst.js';
5
- import pluralize from 'pluralize';
6
- import addCommonTerms from './addCommonTerms';
7
- addCommonTerms();
8
- console.log('WoRD', pluralize('entity'));
9
- export default function replaceOccurrences(code, replacementSingular) {
10
- const replacementPlural = pluralize(replacementSingular);
11
- // Different cases of the replacement string
12
- const replacements = {
13
- camelPlural: camelCase(replacementPlural),
14
- camel: camelCase(replacementSingular),
15
- pascalPlural: upperFirst(camelCase(replacementPlural)),
16
- pascal: upperFirst(camelCase(replacementSingular)),
17
- kebabPlural: kebabCase(replacementPlural),
18
- kebab: kebabCase(replacementSingular),
19
- snakePlural: snakeCase(replacementPlural),
20
- snake: snakeCase(replacementSingular),
21
- screamingSnakePlural: snakeCase(replacementPlural).toUpperCase(),
22
- screamingSnake: snakeCase(replacementSingular).toUpperCase(),
23
- screamingKebabPlural: kebabCase(replacementPlural).toUpperCase(),
24
- screamingKebab: kebabCase(replacementSingular).toUpperCase(),
25
- };
26
- // Create a map of original patterns to their replacements
27
- const originalPatterns = {
28
- camelPlural: 'myThings',
29
- camel: 'myThing',
30
- pascalPlural: 'MyThings',
31
- pascal: 'MyThing',
32
- kebabPlural: 'my-things',
33
- kebab: 'my-thing',
34
- snakePlural: 'my_things',
35
- snake: 'my_thing',
36
- screamingSnakePlural: 'MY_THINGS',
37
- screamingSnake: 'MY_THING',
38
- screamingKebabPlural: 'MY-THINGS',
39
- screamingKebab: 'MY-THING',
40
- };
41
- // Replace all occurrences in the code
42
- Object.keys(originalPatterns).forEach((key) => {
43
- const pattern = originalPatterns[key];
44
- const replacementValue = replacements[key];
45
- const regex = new RegExp(pattern, 'g');
46
- code = code.replace(regex, replacementValue);
47
- });
48
- return code;
49
- }