vovk-cli 0.0.1-draft.130 → 0.0.1-draft.131

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.
@@ -19,7 +19,6 @@ import isSegmentSchemaEmpty from './isSegmentSchemaEmpty.mjs';
19
19
  import writeConfigJson from './writeConfigJson.mjs';
20
20
  export class VovkDev {
21
21
  #projectInfo;
22
- #segments = [];
23
22
  #fullSchema = {
24
23
  segments: {},
25
24
  config: {},
@@ -44,10 +43,10 @@ export class VovkDev {
44
43
  log.debug(`File ${filePath} has been added to segments folder`);
45
44
  if (segmentReg.test(filePath)) {
46
45
  const segmentName = getSegmentName(filePath);
47
- this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
48
- ? this.#segments
46
+ this.#projectInfo.segments = this.#projectInfo.segments.find((s) => s.segmentName === segmentName)
47
+ ? this.#projectInfo.segments
49
48
  : [
50
- ...this.#segments,
49
+ ...this.#projectInfo.segments,
51
50
  {
52
51
  routeFilePath: filePath,
53
52
  segmentName,
@@ -55,8 +54,8 @@ export class VovkDev {
55
54
  },
56
55
  ];
57
56
  log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
58
- log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
59
- void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
57
+ log.debug(`Full list of segments: ${this.#projectInfo.segments.map((s) => s.segmentName).join(', ')}`);
58
+ void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#projectInfo.segments.map((s) => s.segmentName));
60
59
  }
61
60
  })
62
61
  .on('change', (filePath) => {
@@ -67,15 +66,15 @@ export class VovkDev {
67
66
  })
68
67
  .on('addDir', async (dirPath) => {
69
68
  log.debug(`Directory ${dirPath} has been added to segments folder`);
70
- this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
71
- for (const { segmentName } of this.#segments) {
69
+ this.#projectInfo.segments = await locateSegments({ dir: apiDirAbsolutePath, config });
70
+ for (const { segmentName } of this.#projectInfo.segments) {
72
71
  void this.#requestSchema(segmentName);
73
72
  }
74
73
  })
75
74
  .on('unlinkDir', async (dirPath) => {
76
75
  log.debug(`Directory ${dirPath} has been removed from segments folder`);
77
- this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
78
- for (const { segmentName } of this.#segments) {
76
+ this.#projectInfo.segments = await locateSegments({ dir: apiDirAbsolutePath, config });
77
+ for (const { segmentName } of this.#projectInfo.segments) {
79
78
  void this.#requestSchema(segmentName);
80
79
  }
81
80
  })
@@ -83,10 +82,10 @@ export class VovkDev {
83
82
  log.debug(`File ${filePath} has been removed from segments folder`);
84
83
  if (segmentReg.test(filePath)) {
85
84
  const segmentName = getSegmentName(filePath);
86
- this.#segments = this.#segments.filter((s) => s.segmentName !== segmentName);
85
+ this.#projectInfo.segments = this.#projectInfo.segments.filter((s) => s.segmentName !== segmentName);
87
86
  log.info(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} has been removed`);
88
- log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
89
- void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
87
+ log.debug(`Full list of segments: ${this.#projectInfo.segments.map((s) => s.segmentName).join(', ')}`);
88
+ void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#projectInfo.segments.map((s) => s.segmentName));
90
89
  }
91
90
  })
92
91
  .on('ready', () => {
@@ -119,12 +118,12 @@ export class VovkDev {
119
118
  log.debug(`File ${filePath} has been removed from modules folder`);
120
119
  })
121
120
  .on('addDir', () => {
122
- for (const { segmentName } of this.#segments) {
121
+ for (const { segmentName } of this.#projectInfo.segments) {
123
122
  void this.#requestSchema(segmentName);
124
123
  }
125
124
  })
126
125
  .on('unlinkDir', () => {
127
- for (const { segmentName } of this.#segments) {
126
+ for (const { segmentName } of this.#projectInfo.segments) {
128
127
  void this.#requestSchema(segmentName);
129
128
  }
130
129
  })
@@ -192,7 +191,7 @@ export class VovkDev {
192
191
  if (this.#isWatching)
193
192
  throw new Error('Already watching');
194
193
  const { log } = this.#projectInfo;
195
- log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
194
+ log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#projectInfo.segments.map((s) => s.segmentName))}.`);
196
195
  // automatically watches segments and modules
197
196
  this.#watchConfig(callback);
198
197
  }
@@ -207,7 +206,7 @@ export class VovkDev {
207
206
  const namesOfClasses = [...code.matchAll(nameOfClasReg)].map((match) => match[1]);
208
207
  const importRegex = /import\s*{[^}]*\b(get|post|put|del|head|options)\b[^}]*}\s*from\s*['"]vovk['"]/;
209
208
  if (importRegex.test(code) && namesOfClasses.length) {
210
- const affectedSegments = this.#segments.filter((s) => {
209
+ const affectedSegments = this.#projectInfo.segments.filter((s) => {
211
210
  const segmentSchema = this.#fullSchema.segments[s.segmentName];
212
211
  if (!segmentSchema)
213
212
  return false;
@@ -257,7 +256,7 @@ export class VovkDev {
257
256
  }
258
257
  return { isError: false };
259
258
  }, 500);
260
- #generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, fullSchema: this.#fullSchema }).then(this.#onFirstTimeGenerate), 1000);
259
+ #generate = debounce(() => generate({ projectInfo: this.#projectInfo, fullSchema: this.#fullSchema }).then(this.#onFirstTimeGenerate), 1000);
261
260
  async #handleSegmentSchema(segmentName, segmentSchema) {
262
261
  const { log, config, cwd } = this.#projectInfo;
263
262
  if (!segmentSchema) {
@@ -266,7 +265,7 @@ export class VovkDev {
266
265
  }
267
266
  log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
268
267
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
269
- const segment = this.#segments.find((s) => s.segmentName === segmentName);
268
+ const segment = this.#projectInfo.segments.find((s) => s.segmentName === segmentName);
270
269
  if (!segment) {
271
270
  log.warn(`${formatLoggedSegmentName(segmentName)} not found`);
272
271
  return;
@@ -288,7 +287,7 @@ export class VovkDev {
288
287
  else if (segmentSchema && !isSegmentSchemaEmpty(segmentSchema)) {
289
288
  log.error(`Non-empty schema provided for ${formatLoggedSegmentName(segment.segmentName)} but "emitSchema" is false`);
290
289
  }
291
- if (this.#segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
290
+ if (this.#projectInfo.segments.every((s) => this.#fullSchema.segments[s.segmentName])) {
292
291
  log.debug(`All segments with "emitSchema" have schema.`);
293
292
  this.#generate();
294
293
  }
@@ -296,7 +295,7 @@ export class VovkDev {
296
295
  async start({ exit }) {
297
296
  const now = Date.now();
298
297
  this.#projectInfo = await getProjectInfo();
299
- const { log, config, cwd, apiDir } = this.#projectInfo;
298
+ const { log, config, cwd } = this.#projectInfo;
300
299
  log.info('Starting...');
301
300
  if (exit) {
302
301
  this.#onFirstTimeGenerate = once(() => {
@@ -317,16 +316,14 @@ export class VovkDev {
317
316
  process.on('unhandledRejection', (reason) => {
318
317
  log.error(`Unhandled Rejection: ${String(reason)}`);
319
318
  });
320
- const apiDirAbsolutePath = path.join(cwd, apiDir);
321
319
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
322
- this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
323
- await ensureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
320
+ await ensureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#projectInfo.segments.map((s) => s.segmentName));
324
321
  await ensureClient(this.#projectInfo);
325
322
  const MAX_ATTEMPTS = 5;
326
323
  const DELAY = 5000;
327
324
  // Request schema every segment in 5 seconds in order to update schema on start
328
325
  setTimeout(() => {
329
- for (const { segmentName } of this.#segments) {
326
+ for (const { segmentName } of this.#projectInfo.segments) {
330
327
  let attempts = 0;
331
328
  void this.#requestSchema(segmentName).then(({ isError }) => {
332
329
  if (isError) {
@@ -1,4 +1,4 @@
1
1
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- export default function ensureClient({ config, cwd, log }: ProjectInfo): Promise<{
2
+ export default function ensureClient({ config, cwd, log, segments }: ProjectInfo): Promise<{
3
3
  written: boolean;
4
4
  }>;
@@ -1,41 +1,66 @@
1
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
2
3
  import getClientTemplates, { BuiltInTemplateName } from './getClientTemplates.mjs';
3
- import uniq from 'lodash/uniq.js';
4
4
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
5
- import path from 'node:path';
6
- export default async function ensureClient({ config, cwd, log }) {
5
+ import { ROOT_SEGMENT_SCHEMA_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
6
+ async function writeOnePlaceholder({ outPath, defaultText, templateName, usedTemplateNames, }) {
7
+ const existing = await fs.readFile(outPath, 'utf-8').catch(() => null);
8
+ if (!existing) {
9
+ let text = defaultText;
10
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
11
+ // a workaround that prevents compilation error when client is not yet generated but back-end imports fullSchema
12
+ if (Object.keys(BuiltInTemplateName).includes(templateName)) {
13
+ if (outPath.endsWith('.cjs')) {
14
+ text += '\nmodule.exports.fullSchema = {};';
15
+ }
16
+ else {
17
+ text += '\nexport const fullSchema = {};';
18
+ }
19
+ }
20
+ await fs.writeFile(outPath, outPath.endsWith('.py') ? text.replace(/\/\//g, '#') : text);
21
+ usedTemplateNames.add(templateName);
22
+ }
23
+ }
24
+ export default async function ensureClient({ config, cwd, log, segments }) {
7
25
  const now = Date.now();
8
26
  const { clientOutDirAbsolutePath, templateFiles } = await getClientTemplates({
9
27
  config,
10
28
  cwd,
11
29
  generateFrom: config.generateFrom,
12
30
  });
13
- let usedTemplateNames = [];
31
+ const usedTemplateNames = new Set();
14
32
  const defaultText = `// auto-generated ${new Date().toISOString()}
15
33
  // This is a temporary placeholder to avoid compilation errors if client is imported before it's generated.
16
34
  // If you still see this text, the client is not generated yet because of an unknown problem.
17
35
  // Feel free to report an issue at https://github.com/finom/vovk/issues`;
18
- for (const { outPath, templateName } of templateFiles) {
19
- const existing = await fs.readFile(outPath, 'utf-8').catch(() => null);
20
- let text = defaultText;
21
- if (!existing) {
22
- await fs.mkdir(path.dirname(outPath), { recursive: true });
23
- // a workaround that prevents compilation error when client is not yet generated but back-end imports fullSchema
24
- if (Object.keys(BuiltInTemplateName).includes(templateName)) {
25
- if (outPath.endsWith('.cjs')) {
26
- text += '\nmodule.exports.fullSchema = {};';
27
- }
28
- else {
29
- text += '\nexport const fullSchema = {};';
30
- }
31
- }
32
- await fs.writeFile(outPath, outPath.endsWith('.py') ? text.replace(/\/\//g, '#') : text);
33
- usedTemplateNames.push(templateName);
36
+ await Promise.all(templateFiles.map(async (clientTemplate) => {
37
+ const { templatePath, templateName, outDir } = clientTemplate;
38
+ const outPath = path.join(outDir, path.basename(templatePath).replace('.ejs', ''));
39
+ if (config.emitFullClient) {
40
+ await writeOnePlaceholder({
41
+ outPath,
42
+ defaultText,
43
+ templateName,
44
+ usedTemplateNames,
45
+ });
34
46
  }
47
+ if (config.emitSegmentClient) {
48
+ // Generate client files for each segment
49
+ await Promise.all(segments.map(async ({ segmentName }) => {
50
+ const outPath = path.join(outDir, segmentName || ROOT_SEGMENT_SCHEMA_NAME, path.basename(templatePath).replace('.ejs', ''));
51
+ return writeOnePlaceholder({
52
+ outPath,
53
+ defaultText,
54
+ templateName,
55
+ usedTemplateNames,
56
+ });
57
+ }));
58
+ }
59
+ }));
60
+ if (usedTemplateNames.size) {
61
+ log.info(`Placeholder client files from template${usedTemplateNames.size !== 1 ? 's' : ''} ${chalkHighlightThing(Array.from(usedTemplateNames)
62
+ .map((s) => `"${s}"`)
63
+ .join(', '))} are generated at ${clientOutDirAbsolutePath} in ${Date.now() - now}ms`);
35
64
  }
36
- usedTemplateNames = uniq(usedTemplateNames);
37
- if (usedTemplateNames.length) {
38
- log.info(`Placeholder client files from template${usedTemplateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(usedTemplateNames.map((s) => `"${s}"`).join(', '))} are generated at ${clientOutDirAbsolutePath} in ${Date.now() - now}ms`);
39
- }
40
- return { written: !!usedTemplateNames.length };
65
+ return { written: !!usedTemplateNames.size };
41
66
  }
@@ -1,10 +1,10 @@
1
1
  import type { VovkStrictConfig } from 'vovk';
2
2
  export declare const DEFAULT_FULL_SCHEMA_FILE_NAME = "full-schema.json";
3
- interface ClientTemplate {
3
+ export interface ClientTemplate {
4
4
  templateName: string;
5
5
  templatePath: string;
6
- outPath: string;
7
- fullSchemaOutAbsolutePath: string | null;
6
+ outDir: string;
7
+ fullSchemaJSONFileName: string | null;
8
8
  origin?: string | null;
9
9
  }
10
10
  export declare enum BuiltInTemplateName {
@@ -21,4 +21,3 @@ export default function getClientTemplates({ config, cwd, generateFrom, }: {
21
21
  clientOutDirAbsolutePath: string;
22
22
  templateFiles: ClientTemplate[];
23
23
  }>;
24
- export {};
@@ -17,28 +17,28 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
17
17
  templateName: BuiltInTemplateName.ts,
18
18
  templatePath: path.resolve(templatesDir, 'ts/*'),
19
19
  outDir: clientOutDirAbsolutePath,
20
- fullSchema: false,
20
+ fullSchemaJSON: false,
21
21
  origin: null,
22
22
  },
23
23
  main: {
24
24
  templateName: BuiltInTemplateName.main,
25
25
  templatePath: path.resolve(templatesDir, 'main/*'),
26
26
  outDir: clientOutDirAbsolutePath,
27
- fullSchema: false,
27
+ fullSchemaJSON: false,
28
28
  origin: null,
29
29
  },
30
30
  module: {
31
31
  templateName: BuiltInTemplateName.module,
32
32
  templatePath: path.resolve(templatesDir, 'module/*'),
33
33
  outDir: clientOutDirAbsolutePath,
34
- fullSchema: false,
34
+ fullSchemaJSON: false,
35
35
  origin: null,
36
36
  },
37
37
  fullSchema: {
38
38
  templateName: BuiltInTemplateName.fullSchema,
39
39
  templatePath: path.resolve(templatesDir, 'fullSchema/*'),
40
40
  outDir: clientOutDirAbsolutePath,
41
- fullSchema: false,
41
+ fullSchemaJSON: false,
42
42
  origin: null,
43
43
  },
44
44
  };
@@ -51,7 +51,7 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
51
51
  templateName: template,
52
52
  templatePath: resolveAbsoluteModulePath(template, cwd),
53
53
  outDir: clientOutDirAbsolutePath,
54
- fullSchema: false,
54
+ fullSchemaJSON: false,
55
55
  origin: null,
56
56
  };
57
57
  }
@@ -59,7 +59,7 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
59
59
  templateName: template.templateName ?? template.templatePath,
60
60
  templatePath: resolveAbsoluteModulePath(template.templatePath, cwd),
61
61
  outDir: template.outDir ? path.resolve(cwd, template.outDir) : clientOutDirAbsolutePath,
62
- fullSchema: template.fullSchema ?? false,
62
+ fullSchemaJSON: template.fullSchemaJSON ?? false,
63
63
  origin: template.origin ?? null,
64
64
  };
65
65
  });
@@ -70,14 +70,15 @@ export default async function getClientTemplates({ config, cwd, generateFrom = [
70
70
  for (const generateFromItem of generateFromStrict) {
71
71
  const files = await glob(generateFromItem.templatePath);
72
72
  for await (const templatePath of files) {
73
- const fullSchemaOutAbsolutePath = generateFromItem.fullSchema
74
- ? path.resolve(generateFromItem.outDir, generateFromItem.fullSchema === 'string' ? generateFromItem.fullSchema : DEFAULT_FULL_SCHEMA_FILE_NAME)
75
- : null;
76
73
  templateFiles.push({
77
74
  templateName: generateFromItem.templateName,
78
75
  templatePath,
79
- outPath: path.join(generateFromItem.outDir, path.basename(templatePath).replace('.ejs', '')),
80
- fullSchemaOutAbsolutePath,
76
+ outDir: generateFromItem.outDir,
77
+ fullSchemaJSONFileName: generateFromItem.fullSchemaJSON
78
+ ? generateFromItem.fullSchemaJSON === 'string'
79
+ ? generateFromItem.fullSchemaJSON
80
+ : DEFAULT_FULL_SCHEMA_FILE_NAME
81
+ : null,
81
82
  origin: generateFromItem.origin,
82
83
  });
83
84
  }
@@ -1,13 +1,13 @@
1
1
  import type { VovkFullSchema } from 'vovk';
2
2
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
- import type { Segment } from '../locateSegments.mjs';
4
3
  import type { GenerateOptions } from '../types.mjs';
5
- export default function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }: {
4
+ export type ClientImports = {
5
+ fetcher: string;
6
+ validateOnClient: string | null;
7
+ createRPC: string;
8
+ };
9
+ export default function generate({ projectInfo, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }: {
6
10
  projectInfo: ProjectInfo;
7
- segments: Segment[];
8
11
  forceNothingWrittenLog?: boolean;
9
12
  fullSchema: VovkFullSchema;
10
- } & Pick<GenerateOptions, 'templates' | 'prettify' | 'emitFullSchema'>): Promise<{
11
- written: boolean;
12
- path: string;
13
- }>;
13
+ } & Pick<GenerateOptions, 'templates' | 'prettify' | 'emitFullSchema'>): Promise<void>;
@@ -2,114 +2,181 @@ import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
3
  import ejs from 'ejs';
4
4
  import matter from 'gray-matter';
5
- import uniq from 'lodash/uniq.js';
5
+ import _ from 'lodash';
6
6
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
7
7
  import prettify from '../utils/prettify.mjs';
8
8
  import getClientTemplates, { DEFAULT_FULL_SCHEMA_FILE_NAME } from './getClientTemplates.mjs';
9
9
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
10
- import _ from 'lodash';
11
10
  import { ROOT_SEGMENT_SCHEMA_NAME, SEGMENTS_SCHEMA_DIR_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
12
- export default async function generate({ projectInfo, segments, forceNothingWrittenLog, templates, prettify: prettifyClient, fullSchema, emitFullSchema, }) {
13
- const segmentsSchema = fullSchema.segments;
11
+ import pickSegmentFullSchema from '../utils/pickSegmentFullSchema.mjs';
12
+ import removeUnlistedDirectories from '../utils/removeUnlistedDirectories.mjs';
13
+ async function writeOneClientFile({ projectInfo, clientTemplate, fullSchema, prettifyClient, segmentName, imports, templateContent, matterResult: { data, content }, }) {
14
+ const { config, apiRoot, segments } = projectInfo;
15
+ const { templatePath, outDir, origin } = clientTemplate;
16
+ const outPath = path.join(outDir, typeof segmentName === 'string' ? segmentName || ROOT_SEGMENT_SCHEMA_NAME : '', path.basename(templatePath).replace('.ejs', ''));
17
+ const schemaOutDir = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/'); // windows fix
18
+ // Data for the EJS templates:
19
+ const t = {
20
+ _, // lodash
21
+ ROOT_SEGMENT_SCHEMA_NAME,
22
+ SEGMENTS_SCHEMA_DIR_NAME,
23
+ apiRoot: origin ? `${origin}/${config.rootEntry}` : apiRoot,
24
+ imports,
25
+ fullSchema,
26
+ schemaOutDir,
27
+ segmentMeta: Object.fromEntries(segments.map(({ segmentName, routeFilePath, segmentImportPath }) => [
28
+ segmentName,
29
+ {
30
+ routeFilePath,
31
+ segmentImportPath: typeof segmentName === 'string'
32
+ ? `${_.times((segmentName.match(/\//g)?.length ?? 0) + 1)
33
+ .map(() => '..')
34
+ .join('/')}/${segmentImportPath}`
35
+ : segmentImportPath,
36
+ },
37
+ ])),
38
+ };
39
+ if (data.imports instanceof Array) {
40
+ for (const imp of data.imports) {
41
+ t.imports = {
42
+ ...t.imports,
43
+ [imp]: await import(imp),
44
+ };
45
+ }
46
+ }
47
+ // Render the template
48
+ let rendered = templatePath.endsWith('.ejs')
49
+ ? ejs.render(content, { t }, {
50
+ filename: templatePath,
51
+ })
52
+ : templateContent;
53
+ // Optionally prettify
54
+ if (prettifyClient || config.prettifyClient) {
55
+ rendered = await prettify(rendered, outPath);
56
+ }
57
+ // Read existing file content to compare
58
+ const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
59
+ // Determine if we need to rewrite the file, ignore 1st line
60
+ const needsWriting = existingContent.trim().split('\n').slice(1).join('\n') !== rendered.trim().split('\n').slice(1).join('\n');
61
+ if (needsWriting) {
62
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
63
+ await fs.writeFile(outPath, rendered);
64
+ }
65
+ return { written: needsWriting };
66
+ }
67
+ export default async function generate({ projectInfo, forceNothingWrittenLog, templates, prettify: prettifyClient = false, fullSchema, emitFullSchema, }) {
68
+ const now = Date.now();
14
69
  const generateFrom = templates ?? projectInfo.config.generateFrom;
15
70
  const noClient = templates?.[0] === 'none';
16
- const { config, cwd, log, clientImports, apiRoot } = projectInfo;
71
+ const { config, cwd, log, clientImports, segments } = projectInfo;
17
72
  const { clientOutDirAbsolutePath, templateFiles } = await getClientTemplates({ config, cwd, generateFrom });
18
73
  // Ensure that each segment has a matching schema if it needs to be emitted:
19
74
  for (let i = 0; i < segments.length; i++) {
20
75
  const { segmentName } = segments[i];
21
- const schema = segmentsSchema[segmentName];
76
+ const schema = fullSchema.segments[segmentName];
22
77
  if (!schema) {
23
78
  throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
24
79
  }
25
80
  if (!schema.emitSchema)
26
81
  continue;
27
82
  }
28
- const now = Date.now();
29
- const schemaOutDir = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/'); // windows fix
83
+ let written = false;
84
+ const fullSchemaOutAbsolutePaths = new Map();
85
+ const usedTemplateNames = new Set();
30
86
  // Process each template in parallel
31
- const processedTemplates = noClient
32
- ? []
33
- : await Promise.all(templateFiles.map(async ({ templatePath, outPath, templateName, origin }) => {
34
- /*const parsed = matter((await ejs.render(codeTemplate, { t }, { async: true, filename: templateFileName })).trim());
35
- const { dir, fileName, sourceName, compiledName } = parsed.data as VovkModuleRenderResult;
36
- const code = empty ? (sourceName ? `export default class ${sourceName} {}` : '') : parsed.content;*/
87
+ await Promise.all(templateFiles.map(async (clientTemplate) => {
88
+ const { templatePath, templateName, outDir, fullSchemaJSONFileName } = clientTemplate;
89
+ if (!noClient) {
37
90
  // Read the EJS template
38
91
  const templateContent = await fs.readFile(templatePath, 'utf-8');
39
- const { data, content } = matter(templateContent);
40
- // Data for the EJS templates:
41
- const t = {
42
- _, // lodash
43
- ROOT_SEGMENT_SCHEMA_NAME,
44
- SEGMENTS_SCHEMA_DIR_NAME,
45
- apiRoot: origin ? `${origin}/${config.rootEntry}` : apiRoot,
46
- imports: clientImports,
47
- fullSchema,
48
- schemaOutDir,
49
- segmentMeta: Object.fromEntries(segments.map(({ segmentName, ...s }) => [segmentName, s])),
50
- };
51
- if (data.imports instanceof Array) {
52
- for (const imp of data.imports) {
53
- t.imports = {
54
- ...t.imports,
55
- [imp]: await import(imp),
56
- };
92
+ const matterResult = matter(templateContent);
93
+ if (config.emitFullClient) {
94
+ const { written: isWritten } = await writeOneClientFile({
95
+ projectInfo,
96
+ clientTemplate,
97
+ fullSchema,
98
+ prettifyClient,
99
+ segmentName: null,
100
+ imports: clientImports.fullClient,
101
+ templateContent,
102
+ matterResult,
103
+ });
104
+ if (isWritten) {
105
+ usedTemplateNames.add(templateName);
57
106
  }
107
+ written ||= isWritten;
58
108
  }
59
- // Render the template
60
- let rendered = templatePath.endsWith('.ejs')
61
- ? ejs.render(content, { t }, {
62
- filename: templatePath,
63
- })
64
- : templateContent;
65
- // Optionally prettify
66
- if (prettifyClient || config.prettifyClient) {
67
- rendered = await prettify(rendered, outPath);
109
+ // TODO Remove files if emitFullClient is false ???
110
+ if (config.emitSegmentClient) {
111
+ // Generate client files for each segment
112
+ await Promise.all(segments.map(async ({ segmentName }) => {
113
+ const segmentFullSchema = {
114
+ config: fullSchema.config,
115
+ segments: { [segmentName]: fullSchema.segments[segmentName] },
116
+ };
117
+ const { written: isWritten } = await writeOneClientFile({
118
+ projectInfo,
119
+ clientTemplate,
120
+ fullSchema: segmentFullSchema,
121
+ prettifyClient,
122
+ segmentName,
123
+ imports: clientImports.schemaClient[segmentName],
124
+ templateContent,
125
+ matterResult,
126
+ });
127
+ if (isWritten) {
128
+ usedTemplateNames.add(templateName);
129
+ }
130
+ written ||= isWritten;
131
+ }));
132
+ await removeUnlistedDirectories(outDir, segments.map(({ segmentName }) => segmentName || ROOT_SEGMENT_SCHEMA_NAME));
68
133
  }
69
- // Read existing file content to compare
70
- const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
71
- // Determine if we need to rewrite the file, ignore 1st line
72
- const needsWriting = existingContent.trim().split('\n').slice(1).join('\n') !== rendered.trim().split('\n').slice(1).join('\n');
73
- return {
74
- outPath,
75
- rendered,
76
- needsWriting,
77
- templateName,
78
- };
79
- }));
80
- const usedTemplateNames = uniq(processedTemplates.filter(({ needsWriting }) => needsWriting).map(({ templateName }) => templateName));
81
- let schemaPaths = templateFiles
82
- .map(({ fullSchemaOutAbsolutePath }) => fullSchemaOutAbsolutePath)
83
- .filter(Boolean);
134
+ else {
135
+ await removeUnlistedDirectories(outDir, []);
136
+ }
137
+ }
138
+ if (config.emitFullClient) {
139
+ const fullSchemaOutAbsolutePath = fullSchemaJSONFileName ? path.resolve(outDir, fullSchemaJSONFileName) : null;
140
+ if (fullSchemaOutAbsolutePath) {
141
+ fullSchemaOutAbsolutePaths.set(null, fullSchemaOutAbsolutePath);
142
+ }
143
+ }
144
+ if (config.emitSegmentClient) {
145
+ segments.forEach(({ segmentName }) => {
146
+ const fullSchemaOutAbsolutePath = fullSchemaJSONFileName
147
+ ? path.resolve(outDir, segmentName || ROOT_SEGMENT_SCHEMA_NAME, fullSchemaJSONFileName)
148
+ : null;
149
+ if (fullSchemaOutAbsolutePath) {
150
+ fullSchemaOutAbsolutePaths.set(segmentName, fullSchemaOutAbsolutePath);
151
+ }
152
+ });
153
+ }
154
+ }));
84
155
  if (emitFullSchema) {
85
156
  const fullSchemaOutAbsolutePath = emitFullSchema
86
157
  ? path.resolve(clientOutDirAbsolutePath, emitFullSchema === 'string' ? emitFullSchema : DEFAULT_FULL_SCHEMA_FILE_NAME)
87
158
  : null;
88
159
  if (fullSchemaOutAbsolutePath) {
89
- schemaPaths.push(fullSchemaOutAbsolutePath);
160
+ fullSchemaOutAbsolutePaths.set(null, fullSchemaOutAbsolutePath);
90
161
  }
91
162
  }
92
- schemaPaths = uniq(schemaPaths);
93
- if (schemaPaths.length) {
94
- await Promise.all(schemaPaths.map(async (fullSchemaOutAbsolutePath) => {
95
- fs.mkdir(path.dirname(fullSchemaOutAbsolutePath), { recursive: true });
96
- return fs.writeFile(fullSchemaOutAbsolutePath, JSON.stringify(fullSchema, null, 2));
163
+ if (fullSchemaOutAbsolutePaths.size) {
164
+ await Promise.all(Array.from(fullSchemaOutAbsolutePaths.entries()).map(async ([segmentName, outPath]) => {
165
+ const schema = segmentName ? pickSegmentFullSchema(fullSchema, segmentName) : fullSchema;
166
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
167
+ await fs.writeFile(outPath, JSON.stringify(schema, null, 2));
97
168
  }));
98
- log.info(`Full schema has been written to ${schemaPaths.map((s) => `"${s}"`).join(', ')}`);
169
+ log.info(`Full schema has been written to ${Array.from(fullSchemaOutAbsolutePaths)
170
+ .map((s) => `"${s}"`)
171
+ .join(', ')}`);
172
+ }
173
+ if (written) {
174
+ log.info(`Client generated from template${usedTemplateNames.size !== 1 ? 's' : ''} ${chalkHighlightThing(Array.from(usedTemplateNames)
175
+ .map((s) => `"${s}"`)
176
+ .join(', '))} in ${Date.now() - now}ms`);
99
177
  }
100
- if (usedTemplateNames.length === 0) {
178
+ else {
101
179
  const logOrDebug = forceNothingWrittenLog ? log.info : log.debug;
102
180
  logOrDebug(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
103
- return { written: false, path: clientOutDirAbsolutePath };
104
181
  }
105
- // Write updated files where needed
106
- await Promise.all(processedTemplates.map(async ({ outPath, rendered, needsWriting }) => {
107
- if (needsWriting) {
108
- await fs.mkdir(path.dirname(outPath), { recursive: true });
109
- return fs.writeFile(outPath, rendered);
110
- }
111
- return null;
112
- }));
113
- log.info(`Client generated from template${usedTemplateNames.length !== 1 ? 's' : ''} ${chalkHighlightThing(usedTemplateNames.map((s) => `"${s}"`).join(', '))} in ${Date.now() - now}ms`);
114
- return { written: true, path: clientOutDirAbsolutePath };
115
182
  }
@@ -11,6 +11,8 @@ export default async function getConfig({ clientOutDir, configPath, cwd, }) {
11
11
  const defaultClientTemplates = ['module', 'main'];
12
12
  const config = {
13
13
  emitConfig: [],
14
+ emitFullClient: 'VOVK_EMIT_FULL_CLIENT' in env ? !!env.VOVK_EMIT_FULL_CLIENT : (conf.emitFullClient ?? true),
15
+ emitSegmentClient: 'VOVK_EMIT_SEGMENT_CLIENT' in env ? !!env.VOVK_EMIT_SEGMENT_CLIENT : (conf.emitSegmentClient ?? false),
14
16
  modulesDir: env.VOVK_MODULES_DIR ?? conf.modulesDir ?? './' + [srcRoot, 'modules'].filter(Boolean).join('/'),
15
17
  imports: {
16
18
  fetcher: typeof fetcherImport === 'string' ? [fetcherImport] : fetcherImport,
@@ -1,3 +1,4 @@
1
+ import type { ClientImports } from '../generate/index.mjs';
1
2
  export type ProjectInfo = Awaited<ReturnType<typeof getProjectInfo>>;
2
3
  export default function getProjectInfo({ port: givenPort, clientOutDir, configPath, cwd, }?: {
3
4
  port?: number;
@@ -12,14 +13,12 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, configPa
12
13
  srcRoot: string;
13
14
  config: import("vovk").VovkStrictConfig;
14
15
  clientImports: {
15
- fetcher: string;
16
- createRPC: string;
17
- validateOnClient: string | null;
18
- module: {
19
- fetcher: string;
20
- createRPC: string;
21
- validateOnClient: string | null;
16
+ fullClient: ClientImports & {
17
+ module: ClientImports;
22
18
  };
19
+ schemaClient: Record<string, ClientImports & {
20
+ module: ClientImports;
21
+ }>;
23
22
  };
24
23
  log: {
25
24
  info: (msg: string) => void;
@@ -28,4 +27,5 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, configPa
28
27
  debug: (msg: string) => void;
29
28
  raw: import("loglevel").RootLogger;
30
29
  };
30
+ segments: import("../locateSegments.mjs").Segment[];
31
31
  }>;
@@ -1,6 +1,8 @@
1
1
  import path from 'node:path';
2
2
  import getConfig from './getConfig.mjs';
3
3
  import getLogger from '../utils/getLogger.mjs';
4
+ import locateSegments from '../locateSegments.mjs';
5
+ import { ROOT_SEGMENT_SCHEMA_NAME } from '../dev/writeOneSegmentSchemaFile.mjs';
4
6
  export default async function getProjectInfo({ port: givenPort, clientOutDir, configPath, cwd = process.cwd(), } = {}) {
5
7
  const port = givenPort?.toString() ?? process.env.PORT ?? '3000';
6
8
  // Make PORT available to the config file at getConfig
@@ -19,18 +21,40 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, co
19
21
  if (!userConfig && configAbsolutePaths.length > 0) {
20
22
  log.error(`Error reading config file at ${configAbsolutePaths[0]}: ${error?.message ?? 'Unknown Error'}`);
21
23
  }
22
- const getImportPath = (p) => (p.startsWith('.') ? path.relative(config.clientOutDir, p) : p);
24
+ const getImportPath = (p, s = '') => p.startsWith('.') ? path.relative(path.join(config.clientOutDir, s), p) : p;
25
+ const apiDirAbsolutePath = path.join(cwd, apiDir);
26
+ const segments = await locateSegments({ dir: apiDirAbsolutePath, config });
27
+ // TODO Refactor
23
28
  const clientImports = {
24
- fetcher: getImportPath(config.imports.fetcher[0]),
25
- createRPC: getImportPath(config.imports.createRPC[0]),
26
- validateOnClient: config.imports.validateOnClient ? getImportPath(config.imports.validateOnClient[0]) : null,
27
- module: {
28
- fetcher: getImportPath(config.imports.fetcher[1] ?? config.imports.fetcher[0]),
29
- createRPC: getImportPath(config.imports.createRPC[1] ?? config.imports.createRPC[0]),
30
- validateOnClient: config.imports.validateOnClient
31
- ? getImportPath(config.imports.validateOnClient[1] ?? config.imports.validateOnClient[0])
32
- : null,
29
+ fullClient: {
30
+ fetcher: getImportPath(config.imports.fetcher[0]),
31
+ createRPC: getImportPath(config.imports.createRPC[0]),
32
+ validateOnClient: config.imports.validateOnClient ? getImportPath(config.imports.validateOnClient[0]) : null,
33
+ module: {
34
+ fetcher: getImportPath(config.imports.fetcher[1] ?? config.imports.fetcher[0]),
35
+ createRPC: getImportPath(config.imports.createRPC[1] ?? config.imports.createRPC[0]),
36
+ validateOnClient: config.imports.validateOnClient
37
+ ? getImportPath(config.imports.validateOnClient[1] ?? config.imports.validateOnClient[0])
38
+ : null,
39
+ },
33
40
  },
41
+ schemaClient: Object.fromEntries(segments.map((segment) => [
42
+ segment.segmentName,
43
+ {
44
+ fetcher: getImportPath(config.imports.fetcher[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME),
45
+ createRPC: getImportPath(config.imports.createRPC[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME),
46
+ validateOnClient: config.imports.validateOnClient
47
+ ? getImportPath(config.imports.validateOnClient[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME)
48
+ : null,
49
+ module: {
50
+ fetcher: getImportPath(config.imports.fetcher[1] ?? config.imports.fetcher[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME),
51
+ createRPC: getImportPath(config.imports.createRPC[1] ?? config.imports.createRPC[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME),
52
+ validateOnClient: config.imports.validateOnClient
53
+ ? getImportPath(config.imports.validateOnClient[1] ?? config.imports.validateOnClient[0], segment.segmentName || ROOT_SEGMENT_SCHEMA_NAME)
54
+ : null,
55
+ },
56
+ },
57
+ ])),
34
58
  };
35
59
  return {
36
60
  cwd,
@@ -41,5 +65,6 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, co
41
65
  config,
42
66
  clientImports,
43
67
  log,
68
+ segments,
44
69
  };
45
70
  }
package/dist/index.mjs CHANGED
@@ -7,7 +7,6 @@ import concurrently from 'concurrently';
7
7
  import getAvailablePort from './utils/getAvailablePort.mjs';
8
8
  import getProjectInfo from './getProjectInfo/index.mjs';
9
9
  import generate from './generate/index.mjs';
10
- import locateSegments from './locateSegments.mjs';
11
10
  import { VovkDev } from './dev/index.mjs';
12
11
  import newComponents from './new/index.mjs';
13
12
  import initProgram from './initProgram.mjs';
@@ -81,13 +80,11 @@ program
81
80
  .action(async (options) => {
82
81
  const { clientOutDir, templates, prettify, emitFullSchema, config: configPath } = options;
83
82
  const projectInfo = await getProjectInfo({ clientOutDir, configPath });
84
- const { cwd, config, apiDir } = projectInfo;
85
- const segments = await locateSegments({ dir: apiDir, config });
83
+ const { cwd, config } = projectInfo;
86
84
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
87
85
  const fullSchema = await getFullSchemaFromJSON(schemaOutAbsolutePath, projectInfo);
88
86
  await generate({
89
87
  projectInfo,
90
- segments,
91
88
  fullSchema,
92
89
  templates,
93
90
  prettify,
@@ -1,7 +1,6 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import getFileSystemEntryType from './utils/getFileSystemEntryType.mjs';
4
- // config: null is used for testing
5
4
  export default async function locateSegments({ dir, rootDir, config, }) {
6
5
  let results = [];
7
6
  rootDir = rootDir ?? dir;
@@ -19,7 +18,7 @@ export default async function locateSegments({ dir, rootDir, config, }) {
19
18
  if (await getFileSystemEntryType(routeFilePath)) {
20
19
  // Calculate the basePath relative to the root directory
21
20
  const segmentName = path.relative(rootDir, dir).replace(/\\/g, '/'); // windows fix
22
- const segmentImportPath = path.relative(config?.clientOutDir ?? '.__', routeFilePath);
21
+ const segmentImportPath = path.relative(config?.clientOutDir ?? '.__error', routeFilePath);
23
22
  results.push({ routeFilePath, segmentName, segmentImportPath });
24
23
  }
25
24
  }
@@ -0,0 +1,2 @@
1
+ import type { VovkFullSchema } from 'vovk';
2
+ export default function pickSegmentFullSchema(fullSchema: VovkFullSchema, segmentName: string): VovkFullSchema;
@@ -0,0 +1,8 @@
1
+ export default function pickSegmentFullSchema(fullSchema, segmentName) {
2
+ return {
3
+ config: fullSchema.config,
4
+ segments: {
5
+ [segmentName]: fullSchema.segments[segmentName],
6
+ },
7
+ };
8
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Removes all directories in a folder that aren't in the provided allowlist
3
+ * Supports nested directory paths like 'foo/bar/baz'
4
+ *
5
+ * @param folderPath - The path to the folder to process
6
+ * @param allowedDirs - Array of relative directory paths to keep
7
+ * @returns Promise that resolves when all operations are complete
8
+ */
9
+ declare function removeUnlistedDirectories(folderPath: string, allowedDirs: string[]): Promise<void>;
10
+ export default removeUnlistedDirectories;
@@ -0,0 +1,54 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'path';
3
+ /**
4
+ * Removes all directories in a folder that aren't in the provided allowlist
5
+ * Supports nested directory paths like 'foo/bar/baz'
6
+ *
7
+ * @param folderPath - The path to the folder to process
8
+ * @param allowedDirs - Array of relative directory paths to keep
9
+ * @returns Promise that resolves when all operations are complete
10
+ */
11
+ async function removeUnlistedDirectories(folderPath, allowedDirs) {
12
+ // Normalize all allowed paths to use the system-specific separator
13
+ const normalizedAllowedDirs = allowedDirs.map((dir) => dir.split('/').join(path.sep));
14
+ // Process the directory tree recursively
15
+ await processDirectory(folderPath, '', normalizedAllowedDirs);
16
+ }
17
+ /**
18
+ * Recursively processes directories to determine which should be kept or removed
19
+ *
20
+ * @param basePath - The absolute base path being processed
21
+ * @param relativePath - The current relative path from the base
22
+ * @param allowedDirs - Normalized list of allowed directory paths
23
+ */
24
+ async function processDirectory(basePath, relativePath, allowedDirs) {
25
+ const currentDirPath = path.join(basePath, relativePath);
26
+ // Read all entries in the current directory
27
+ const entries = await fs.readdir(currentDirPath, { withFileTypes: true });
28
+ // Process only directories
29
+ const dirEntries = entries.filter((entry) => entry.isDirectory());
30
+ // Check each directory
31
+ for (const dir of dirEntries) {
32
+ // Calculate the new relative path
33
+ const newRelativePath = relativePath ? path.join(relativePath, dir.name) : dir.name;
34
+ // Check if this directory or any of its subdirectories should be kept
35
+ const shouldKeep = allowedDirs.some((allowedDir) => {
36
+ // Direct match
37
+ if (allowedDir === newRelativePath)
38
+ return true;
39
+ // Check if it's a parent path of an allowed directory
40
+ // e.g. "foo" is a parent of "foo/bar/baz"
41
+ return allowedDir.startsWith(newRelativePath + path.sep);
42
+ });
43
+ if (shouldKeep) {
44
+ // Recursively process this directory's contents
45
+ await processDirectory(basePath, newRelativePath, allowedDirs);
46
+ }
47
+ else {
48
+ // Remove this directory since it's not in the allowed list
49
+ const fullPath = path.join(basePath, newRelativePath);
50
+ await fs.rm(fullPath, { recursive: true, force: true });
51
+ }
52
+ }
53
+ }
54
+ export default removeUnlistedDirectories;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.130",
3
+ "version": "0.0.1-draft.131",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "homepage": "https://vovk.dev",
37
37
  "peerDependencies": {
38
- "vovk": "^3.0.0-draft.109"
38
+ "vovk": "^3.0.0-draft.110"
39
39
  },
40
40
  "optionalDependencies": {
41
41
  "vovk-python-client": "*"