vovk-cli 0.0.1-draft.42 → 0.0.1-draft.44

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.
@@ -1,9 +1,11 @@
1
+
1
2
  <%- '// auto-generated\n/* eslint-disable */' %>
2
- import type { clientizeController, VovkClientFetcher } from 'vovk/client';
3
- import type { promisifyWorker } from 'vovk/worker';
3
+ import type { createRPC, createWPC, VovkClientFetcher } from 'vovk';
4
4
  import type fetcher from '<%= fetcherClientImportPath %>';
5
- <% segments.forEach((segment, i) => { %>
6
- import type { Controllers as Controllers<%= i %>, Workers as Workers<%= i %> } from "<%= segment.segmentImportPath %>";
5
+ <% segments.forEach((segment, i) => {
6
+ const hasWorkers = !!Object.keys(segmentsSchema[segment.segmentName].workers).length;
7
+ %>
8
+ import type { Controllers as Controllers<%= i %><% if(hasWorkers) { %>, Workers as Workers<%= i %> <% } %> } from "<%= segment.segmentImportPath %>";
7
9
  <% }) %>
8
10
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
9
11
 
@@ -14,9 +16,9 @@ type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
14
16
  const workers = Object.keys(segSchema.workers);
15
17
  %>
16
18
  <% controllers.forEach((key) => { %>
17
- export const <%= key %>: ReturnType<typeof clientizeController<Controllers<%= i %>["<%= key %>"], Options>>;
19
+ export const <%= key %>: ReturnType<typeof createRPC<Controllers<%= i %>["<%= key %>"], Options>>;
18
20
  <% }) %>
19
21
  <% workers.forEach((key) => { %>
20
- export const <%= key %>: ReturnType<typeof promisifyWorker<Workers<%= i %>["<%= key %>"]>>;
22
+ export const <%= key %>: ReturnType<typeof createWPC<Workers<%= i %>["<%= key %>"]>>;
21
23
  <% }) %>
22
24
  <% }) %>
@@ -1,11 +1,10 @@
1
1
  <%- '// auto-generated\n/* eslint-disable */' %>
2
- const { clientizeController } = require('vovk/client');
3
- const { promisifyWorker } = require('vovk/worker');
2
+ const { createRPC, createWPC } = require('vovk');
4
3
  const { default: fetcher } = require('<%= fetcherClientImportPath %>');
5
4
  const schema = require('<%= schemaOutImportPath %>');
6
5
 
7
6
  const { default: validateOnClient = null } = <%- validateOnClientImportPath ? `require('${validateOnClientImportPath}')` : '{}'%>;
8
- const apiRoot = '<%= apiEntryPoint %>';
7
+ const apiRoot = '<%= apiRoot %>';
9
8
 
10
9
  <% segments.forEach((segment) => {
11
10
  const segSchema = segmentsSchema[segment.segmentName];
@@ -14,13 +13,13 @@ const apiRoot = '<%= apiEntryPoint %>';
14
13
  const workers = Object.keys(segSchema.workers);
15
14
  %>
16
15
  <% controllers.forEach((key) => { %>
17
- exports.<%= key %> = clientizeController(
16
+ exports.<%= key %> = createRPC(
18
17
  schema['<%= segment.segmentName %>'].controllers.<%= key %>,
19
18
  '<%= segment.segmentName %>',
20
19
  { fetcher, validateOnClient, defaultOptions: { apiRoot } }
21
20
  );
22
21
  <% }) %>
23
22
  <% workers.forEach((key) => { %>
24
- exports.<%= key %> = promisifyWorker(null, schema['<%= segment.segmentName %>'].workers.<%= key %>);
23
+ exports.<%= key %> = createWPC(null, schema['<%= segment.segmentName %>'].workers.<%= key %>);
25
24
  <% }) %>
26
25
  <% }) %>
@@ -1,6 +1,5 @@
1
1
  <%- '// auto-generated\n/* eslint-disable */' %>
2
- import { clientizeController, type VovkClientFetcher } from 'vovk/client';
3
- import { promisifyWorker } from 'vovk/worker';
2
+ import { createRPC, createWPC, type VovkClientFetcher } from 'vovk';
4
3
  import fetcher from '<%= fetcherClientImportPath %>';
5
4
  import schema from '<%= schemaOutImportPath %>';
6
5
  <% if (validateOnClientImportPath) { %>
@@ -9,16 +8,17 @@ import validateOnClient from '<%= validateOnClientImportPath %>';
9
8
  const validateOnClient = undefined;
10
9
  <% } %>
11
10
  type Options = typeof fetcher extends VovkClientFetcher<infer U> ? U : never;
12
- const apiRoot = '<%= apiEntryPoint %>';
11
+ const apiRoot = '<%= apiRoot %>';
13
12
 
14
13
  <% segments.forEach((segment, i) => {
15
14
  const segSchema = segmentsSchema[segment.segmentName];
16
15
  if (!segSchema || !segSchema.emitSchema) return;
16
+ const hasWorkers = !!Object.keys(segmentsSchema[segment.segmentName].workers).length;
17
17
  %>
18
- import type { Controllers as Controllers<%= i %>, Workers as Workers<%= i %> } from "<%= segment.segmentImportPath %>";
18
+ import type { Controllers as Controllers<%= i %><% if(hasWorkers) { %>, Workers as Workers<%= i %> <% } %>} from "<%= segment.segmentImportPath %>";
19
19
 
20
20
  <% Object.keys(segSchema.controllers).forEach((key) => { %>
21
- export const <%= key %> = clientizeController<Controllers<%= i %>["<%= key %>"], Options>(
21
+ export const <%= key %> = createRPC<Controllers<%= i %>["<%= key %>"], Options>(
22
22
  schema['<%= segment.segmentName %>'].controllers.<%= key %>,
23
23
  '<%= segment.segmentName %>',
24
24
  { fetcher, validateOnClient, defaultOptions: { apiRoot } }
@@ -26,7 +26,7 @@ export const <%= key %> = clientizeController<Controllers<%= i %>["<%= key %>"],
26
26
  <% }) %>
27
27
 
28
28
  <% Object.keys(segSchema.workers).forEach((key) => { %>
29
- export const <%= key %> = promisifyWorker<Workers<%= i %>["<%= key %>"]>(
29
+ export const <%= key %> = createWPC<Workers<%= i %>["<%= key %>"]>(
30
30
  null,
31
31
  schema['<%= segment.segmentName %>'].workers.<%= key %>
32
32
  );
@@ -1,5 +1,4 @@
1
- import type { VovkSchema } from 'vovk';
2
- import type { _VovkControllerSchema, _VovkWorkerSchema } from 'vovk/types';
1
+ import type { VovkControllerSchema, VovkWorkerSchema, VovkSchema } from 'vovk';
3
2
  interface HandlersDiff {
4
3
  nameOfClass: string;
5
4
  added: string[];
@@ -15,7 +14,7 @@ export interface DiffResult {
15
14
  workers: WorkersOrControllersDiff;
16
15
  controllers: WorkersOrControllersDiff;
17
16
  }
18
- export declare function diffHandlers<T extends _VovkWorkerSchema['handlers'] | _VovkControllerSchema['handlers']>(oldHandlers: T, newHandlers: T, nameOfClass: string): HandlersDiff;
17
+ export declare function diffHandlers<T extends VovkWorkerSchema['handlers'] | VovkControllerSchema['handlers']>(oldHandlers: T, newHandlers: T, nameOfClass: string): HandlersDiff;
19
18
  export declare function diffWorkersOrControllers<T extends VovkSchema['controllers'] | VovkSchema['workers']>(oldItems: T, newItems: T): WorkersOrControllersDiff;
20
19
  /**
21
20
  example output:
@@ -1,5 +1,5 @@
1
1
  import type { ProjectInfo } from '../getProjectInfo/index.mjs';
2
- export default function ensureClient(projectInfo: ProjectInfo): Promise<{
2
+ export default function ensureClient({ config, cwd, log }: ProjectInfo): Promise<{
3
3
  written: boolean;
4
4
  path: string;
5
5
  }>;
@@ -1,7 +1,6 @@
1
1
  import path from 'node:path';
2
2
  import fs from 'node:fs/promises';
3
- export default async function ensureClient(projectInfo) {
4
- const { config, cwd, log } = projectInfo;
3
+ export default async function ensureClient({ config, cwd, log }) {
5
4
  const now = Date.now();
6
5
  const clientoOutDirAbsolutePath = path.join(cwd, config.clientOutDir);
7
6
  const dts = `// auto-generated
@@ -10,8 +9,8 @@ export default async function ensureClient(projectInfo) {
10
9
  // Feel free to report an issue at https://github.com/finom/vovk/issues`;
11
10
  const js = dts;
12
11
  const ts = dts;
13
- const localJsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'client.js');
14
- const localDtsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'client.d.ts');
12
+ const localJsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'compiled.js');
13
+ const localDtsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'compiled.d.ts');
15
14
  const localTsAbsolutePath = path.join(clientoOutDirAbsolutePath, 'index.ts');
16
15
  const existingJs = await fs.readFile(localJsAbsolutePath, 'utf-8').catch(() => null);
17
16
  const existingDts = await fs.readFile(localDtsAbsolutePath, 'utf-8').catch(() => null);
@@ -1,3 +1,6 @@
1
1
  import { ProjectInfo } from '../getProjectInfo/index.mjs';
2
+ /**
3
+ * Ensure that the schema files are created to avoid any import errors.
4
+ */
2
5
  export default function ensureSchemaFiles(projectInfo: ProjectInfo | null, schemaOutAbsolutePath: string, segmentNames: string[]): Promise<void>;
3
6
  export declare const debouncedEnsureSchemaFiles: import("lodash").DebouncedFunc<typeof ensureSchemaFiles>;
@@ -3,6 +3,9 @@ import path from 'node: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
+ /**
7
+ * Ensure that the schema files are created to avoid any import errors.
8
+ */
6
9
  export default async function ensureSchemaFiles(projectInfo, schemaOutAbsolutePath, segmentNames) {
7
10
  const now = Date.now();
8
11
  let hasChanged = false;
@@ -78,6 +81,6 @@ export default segmentSchema;`;
78
81
  // Start the recursive deletion from the root directory
79
82
  await deleteUnnecessaryJsonFiles(schemaOutAbsolutePath);
80
83
  if (hasChanged)
81
- projectInfo?.log.info(`Schema files updated in ${Date.now() - now}ms`);
84
+ projectInfo?.log.info(`Created empty schema files in ${Date.now() - now}ms`);
82
85
  }
83
86
  export const debouncedEnsureSchemaFiles = debounce(ensureSchemaFiles, 1000);
@@ -1,4 +1,6 @@
1
1
  export declare class VovkDev {
2
2
  #private;
3
- start(): Promise<void>;
3
+ start({ exit }: {
4
+ exit: boolean;
5
+ }): Promise<void>;
4
6
  }
@@ -6,12 +6,13 @@ import keyBy from 'lodash/keyBy.js';
6
6
  import capitalize from 'lodash/capitalize.js';
7
7
  import debounce from 'lodash/debounce.js';
8
8
  import isEmpty from 'lodash/isEmpty.js';
9
+ import once from 'lodash/once.js';
9
10
  import { debouncedEnsureSchemaFiles } from './ensureSchemaFiles.mjs';
10
11
  import writeOneSchemaFile from './writeOneSchemaFile.mjs';
11
12
  import logDiffResult from './logDiffResult.mjs';
12
13
  import ensureClient from './ensureClient.mjs';
13
14
  import getProjectInfo from '../getProjectInfo/index.mjs';
14
- import generateClient from '../generateClient.mjs';
15
+ import generate from '../generate/index.mjs';
15
16
  import locateSegments from '../locateSegments.mjs';
16
17
  import debounceWithArgs from '../utils/debounceWithArgs.mjs';
17
18
  import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
@@ -22,6 +23,7 @@ export class VovkDev {
22
23
  #isWatching = false;
23
24
  #modulesWatcher = null;
24
25
  #segmentWatcher = null;
26
+ #onFirstTimeGenerate = null;
25
27
  #watchSegments = (callback) => {
26
28
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
27
29
  const { cwd, log, config, apiDir } = this.#projectInfo;
@@ -40,11 +42,14 @@ export class VovkDev {
40
42
  const segmentName = getSegmentName(filePath);
41
43
  this.#segments = this.#segments.find((s) => s.segmentName === segmentName)
42
44
  ? this.#segments
43
- : [...this.#segments, {
45
+ : [
46
+ ...this.#segments,
47
+ {
44
48
  routeFilePath: filePath,
45
49
  segmentName,
46
- segmentImportPath: path.relative(config.clientOutDir, filePath) // TODO DRY locateSegments
47
- }];
50
+ segmentImportPath: path.relative(config.clientOutDir, filePath), // TODO DRY locateSegments
51
+ },
52
+ ];
48
53
  log.info(`${capitalize(formatLoggedSegmentName(segmentName))} has been added`);
49
54
  log.debug(`Full list of segments: ${this.#segments.map((s) => s.segmentName).join(', ')}`);
50
55
  void debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
@@ -138,7 +143,7 @@ export class VovkDev {
138
143
  await this.#segmentWatcher?.close();
139
144
  await Promise.all([
140
145
  new Promise((resolve) => this.#watchModules(() => resolve(0))),
141
- new Promise((resolve) => this.#watchSegments(() => resolve(0)))
146
+ new Promise((resolve) => this.#watchSegments(() => resolve(0))),
142
147
  ]);
143
148
  if (isInitial) {
144
149
  callback();
@@ -150,7 +155,14 @@ export class VovkDev {
150
155
  }, 1000);
151
156
  chokidar
152
157
  // .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
153
- .watch(['vovk.config.js', 'vovk.config.mjs', 'vovk.config.cjs', '.config/vovk.config.js', '.config/vovk.config.mjs', '.config/vovk.config.cjs'], {
158
+ .watch([
159
+ 'vovk.config.js',
160
+ 'vovk.config.mjs',
161
+ 'vovk.config.cjs',
162
+ '.config/vovk.config.js',
163
+ '.config/vovk.config.mjs',
164
+ '.config/vovk.config.cjs',
165
+ ], {
154
166
  persistent: true,
155
167
  cwd,
156
168
  ignoreInitial: false,
@@ -215,9 +227,9 @@ export class VovkDev {
215
227
  }
216
228
  };
217
229
  #requestSchema = debounceWithArgs(async (segmentName) => {
218
- const { apiEntryPoint, log, port, config } = this.#projectInfo;
230
+ const { apiRoot, log, port, config } = this.#projectInfo;
219
231
  const { devHttps } = config;
220
- const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
232
+ const endpoint = `${apiRoot.startsWith(`http${devHttps ? 's' : ''}://`) ? apiRoot : `http${devHttps ? 's' : ''}://localhost:${port}${apiRoot}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
221
233
  log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
222
234
  try {
223
235
  const resp = await fetch(endpoint);
@@ -238,11 +250,12 @@ export class VovkDev {
238
250
  await this.#handleSchema(schema);
239
251
  }
240
252
  catch (error) {
241
- log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
253
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}: ${error.message}`);
242
254
  return { isError: true };
243
255
  }
244
256
  return { isError: false };
245
257
  }, 500);
258
+ #generate = debounce(() => generate({ projectInfo: this.#projectInfo, segments: this.#segments, segmentsSchema: this.#schemas }).then(this.#onFirstTimeGenerate), 1000);
246
259
  async #handleSchema(schema) {
247
260
  const { log, config, cwd } = this.#projectInfo;
248
261
  if (!schema) {
@@ -275,14 +288,19 @@ export class VovkDev {
275
288
  }
276
289
  if (this.#segments.every((s) => this.#schemas[s.segmentName])) {
277
290
  log.debug(`All segments with "emitSchema" have schema.`);
278
- await generateClient({ projectInfo: this.#projectInfo, segments: this.#segments, segmentsSchema: this.#schemas });
291
+ this.#generate();
279
292
  }
280
293
  }
281
- async start() {
294
+ async start({ exit }) {
282
295
  const now = Date.now();
283
296
  this.#projectInfo = await getProjectInfo();
284
297
  const { log, config, cwd, apiDir } = this.#projectInfo;
285
298
  log.info('Starting...');
299
+ if (exit) {
300
+ this.#onFirstTimeGenerate = once(() => {
301
+ log.info('The schemas and the RPC client have been generated. Exiting...');
302
+ });
303
+ }
286
304
  if (config.devHttps) {
287
305
  const agent = new Agent({
288
306
  connect: {
@@ -301,10 +319,11 @@ export class VovkDev {
301
319
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
302
320
  this.#segments = await locateSegments({ dir: apiDirAbsolutePath, config });
303
321
  await debouncedEnsureSchemaFiles(this.#projectInfo, schemaOutAbsolutePath, this.#segments.map((s) => s.segmentName));
322
+ const MAX_ATTEMPTS = 5;
323
+ const DELAY = 5000;
304
324
  // Request schema every segment in 5 seconds in order to update schema on start
305
325
  setTimeout(() => {
306
326
  for (const { segmentName } of this.#segments) {
307
- const MAX_ATTEMPTS = 3;
308
327
  let attempts = 0;
309
328
  void this.#requestSchema(segmentName).then(({ isError }) => {
310
329
  if (isError) {
@@ -318,19 +337,25 @@ export class VovkDev {
318
337
  void this.#requestSchema(segmentName).then(({ isError: isError2 }) => {
319
338
  if (!isError2) {
320
339
  clearInterval(interval);
340
+ log.info(`Requested schema for ${formatLoggedSegmentName(segmentName)} after ${attempts} attempts`);
321
341
  }
322
342
  });
323
- }, 5000);
343
+ }, DELAY);
324
344
  }
325
345
  });
326
346
  }
327
- }, 5000);
328
- this.#watch(() => {
329
- log.info(`Ready in ${Date.now() - now}ms`);
330
- });
347
+ }, DELAY);
348
+ if (!exit) {
349
+ this.#watch(() => {
350
+ log.info(`Ready in ${Date.now() - now}ms. Making initial requests for schemas in a moment...`);
351
+ });
352
+ }
353
+ else {
354
+ log.info(`Ready in ${Date.now() - now}ms. Making initial requests for schemas in a moment...`);
355
+ }
331
356
  }
332
357
  }
333
358
  const env = process.env;
334
359
  if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
335
- void new VovkDev().start();
360
+ void new VovkDev().start({ exit: env.__VOVK_EXIT__ === 'true' });
336
361
  }
@@ -46,43 +46,43 @@ export default function logDiffResult(segmentName, diffResult, projectInfo) {
46
46
  case 'worker':
47
47
  switch (diffNormalizedItem.type) {
48
48
  case 'added':
49
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
49
+ projectInfo.log.info(`Schema for WPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
50
50
  break;
51
51
  case 'removed':
52
- projectInfo.log.info(`Schema for worker ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
52
+ projectInfo.log.info(`Schema for WPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
53
53
  break;
54
54
  }
55
55
  break;
56
56
  case 'controller':
57
57
  switch (diffNormalizedItem.type) {
58
58
  case 'added':
59
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
59
+ projectInfo.log.info(`Schema forn RPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
60
60
  break;
61
61
  case 'removed':
62
- projectInfo.log.info(`Schema for controller ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
62
+ projectInfo.log.info(`Schema forn RPC ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
63
63
  break;
64
64
  }
65
65
  break;
66
66
  case 'workerHandler':
67
67
  switch (diffNormalizedItem.type) {
68
68
  case 'added':
69
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
69
+ projectInfo.log.info(`Schema for WPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
70
70
  break;
71
71
  case 'removed':
72
- projectInfo.log.info(`Schema for worker method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
72
+ projectInfo.log.info(`Schema for WPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
73
73
  break;
74
74
  }
75
75
  break;
76
76
  case 'controllerHandler':
77
77
  switch (diffNormalizedItem.type) {
78
78
  case 'added':
79
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
79
+ projectInfo.log.info(`Schema forn RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${addedText} at ${formatLoggedSegmentName(segmentName)}`);
80
80
  break;
81
81
  case 'removed':
82
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
82
+ projectInfo.log.info(`Schema forn RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${removedText} from ${formatLoggedSegmentName(segmentName)}`);
83
83
  break;
84
84
  case 'changed':
85
- projectInfo.log.info(`Schema for controller method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${changedText} at ${formatLoggedSegmentName(segmentName)}`);
85
+ projectInfo.log.info(`Schema forn RPC method ${chalkHighlightThing(diffNormalizedItem.name)} has been ${changedText} at ${formatLoggedSegmentName(segmentName)}`);
86
86
  break;
87
87
  }
88
88
  break;
@@ -0,0 +1,11 @@
1
+ import { VovkConfig } from '../types.mjs';
2
+ interface ClientTemplate {
3
+ templatePath: string;
4
+ outPath: string;
5
+ }
6
+ export default function getClientTemplates({ config, cwd, templateNames, }: {
7
+ config: Required<VovkConfig>;
8
+ cwd: string;
9
+ templateNames?: string[];
10
+ }): ClientTemplate[];
11
+ export {};
@@ -0,0 +1,28 @@
1
+ import path from 'node:path';
2
+ export default function getClientTemplates({ config, cwd, templateNames = [], }) {
3
+ const __dirname = path.dirname(new URL(import.meta.url).pathname);
4
+ const templatesDir = path.join(__dirname, '../..', 'client-templates');
5
+ const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
6
+ const mapper = (dir) => (name) => ({
7
+ templatePath: path.resolve(templatesDir, dir, name),
8
+ outPath: path.join(clientOutDirAbsolutePath, name.replace('.ejs', '')),
9
+ });
10
+ const builtInTemplatesMap = {
11
+ ts: ['index.ts.ejs'].map(mapper('ts')),
12
+ compiled: ['compiled.js.ejs', 'compiled.d.ts.ejs'].map(mapper('compiled')),
13
+ python: ['__init__.py'].map(mapper('python')),
14
+ };
15
+ const templateFiles = (templateNames ?? config.experimental_clientGenerateTemplateNames).reduce((acc, template) => {
16
+ if (template in builtInTemplatesMap) {
17
+ return [...acc, ...builtInTemplatesMap[template]];
18
+ }
19
+ return [
20
+ ...acc,
21
+ {
22
+ templatePath: path.resolve(cwd, template),
23
+ outPath: path.join(clientOutDirAbsolutePath, path.basename(template).replace('.ejs', '')),
24
+ },
25
+ ];
26
+ }, []);
27
+ return templateFiles;
28
+ }
@@ -0,0 +1,12 @@
1
+ import type { VovkSchema } from 'vovk';
2
+ import type { ProjectInfo } from '../getProjectInfo/index.mjs';
3
+ import type { Segment } from '../locateSegments.mjs';
4
+ import { GenerateOptions } from '../types.mjs';
5
+ export default function generate({ projectInfo, segments, segmentsSchema, templates, prettify: prettifyClient, fullSchema, }: {
6
+ projectInfo: ProjectInfo;
7
+ segments: Segment[];
8
+ segmentsSchema: Record<string, VovkSchema>;
9
+ } & Pick<GenerateOptions, 'templates' | 'prettify' | 'fullSchema'>): Promise<{
10
+ written: boolean;
11
+ path: string;
12
+ }>;
@@ -0,0 +1,78 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs/promises';
3
+ import ejs from 'ejs';
4
+ import formatLoggedSegmentName from '../utils/formatLoggedSegmentName.mjs';
5
+ import prettify from '../utils/prettify.mjs';
6
+ import getClientTemplates from './getClientTemplates.mjs';
7
+ export default async function generate({ projectInfo, segments, segmentsSchema, templates, prettify: prettifyClient, fullSchema, }) {
8
+ templates = templates ?? projectInfo.config.experimental_clientGenerateTemplateNames;
9
+ const noClient = templates?.[0] === 'none';
10
+ const { config, cwd, log, validateOnClientImportPath, apiRoot, fetcherClientImportPath, schemaOutImportPath } = projectInfo;
11
+ const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
12
+ const templateFiles = getClientTemplates({ config, cwd, templateNames: templates });
13
+ // Ensure that each segment has a matching schema if it needs to be emitted:
14
+ for (let i = 0; i < segments.length; i++) {
15
+ const { segmentName } = segments[i];
16
+ const schema = segmentsSchema[segmentName];
17
+ if (!schema) {
18
+ throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
19
+ }
20
+ if (!schema.emitSchema)
21
+ continue;
22
+ }
23
+ const now = Date.now();
24
+ // Data for the EJS templates:
25
+ const ejsData = {
26
+ apiRoot,
27
+ fetcherClientImportPath,
28
+ schemaOutImportPath,
29
+ validateOnClientImportPath,
30
+ segments,
31
+ segmentsSchema,
32
+ };
33
+ // Process each template in parallel
34
+ const processedTemplates = noClient
35
+ ? []
36
+ : await Promise.all(templateFiles.map(async ({ templatePath, outPath }) => {
37
+ // Read the EJS template
38
+ const templateContent = await fs.readFile(templatePath, 'utf-8');
39
+ // Render the template
40
+ let rendered = templatePath.endsWith('.ejs') ? ejs.render(templateContent, ejsData) : templateContent;
41
+ // Optionally prettify
42
+ if (prettifyClient || config.prettifyClient) {
43
+ rendered = await prettify(rendered, outPath);
44
+ }
45
+ // Read existing file content to compare
46
+ const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
47
+ // Determine if we need to rewrite the file
48
+ const needsWriting = existingContent !== rendered;
49
+ return {
50
+ outPath,
51
+ rendered,
52
+ needsWriting,
53
+ };
54
+ }));
55
+ const anyNeedsWriting = processedTemplates.some(({ needsWriting }) => needsWriting);
56
+ if (fullSchema || anyNeedsWriting) {
57
+ // Make sure the output directory exists
58
+ await fs.mkdir(clientOutDirAbsolutePath, { recursive: true });
59
+ }
60
+ if (fullSchema) {
61
+ const fullSchemaOutAbsolutePath = path.resolve(clientOutDirAbsolutePath, typeof fullSchema === 'string' ? fullSchema : 'full-schema.json');
62
+ await fs.writeFile(fullSchemaOutAbsolutePath, JSON.stringify(segmentsSchema, null, 2));
63
+ log.info(`Full schema has ben written to ${fullSchemaOutAbsolutePath}`);
64
+ }
65
+ if (!anyNeedsWriting) {
66
+ log.debug(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
67
+ return { written: false, path: clientOutDirAbsolutePath };
68
+ }
69
+ // Write updated files where needed
70
+ await Promise.all(processedTemplates.map(({ outPath, rendered, needsWriting }) => {
71
+ if (needsWriting) {
72
+ return fs.writeFile(outPath, rendered);
73
+ }
74
+ return null;
75
+ }));
76
+ log.info(`Client generated in ${Date.now() - now}ms`);
77
+ return { written: true, path: clientOutDirAbsolutePath };
78
+ }
@@ -9,7 +9,7 @@ export default async function getConfig({ clientOutDir, cwd }) {
9
9
  modulesDir: env.VOVK_MODULES_DIR ?? conf.modulesDir ?? './' + [srcRoot, 'modules'].filter(Boolean).join('/'),
10
10
  validateOnClient: env.VOVK_VALIDATE_ON_CLIENT ?? conf.validateOnClient ?? null,
11
11
  validationLibrary: env.VOVK_VALIDATION_LIBRARY ?? conf.validationLibrary ?? null,
12
- fetcher: env.VOVK_FETCHER ?? conf.fetcher ?? 'vovk/client/defaultFetcher',
12
+ fetcher: env.VOVK_FETCHER ?? conf.fetcher ?? 'vovk/dist/client/defaultFetcher',
13
13
  schemaOutDir: env.VOVK_SCHEMA_OUT_DIR ?? conf.schemaOutDir ?? './.vovk-schema',
14
14
  clientOutDir: clientOutDir ?? env.VOVK_CLIENT_OUT_DIR ?? conf.clientOutDir ?? './node_modules/.vovk-client',
15
15
  origin: (env.VOVK_ORIGIN ?? conf.origin ?? '').replace(/\/$/, ''), // Remove trailing slash
@@ -18,6 +18,7 @@ export default async function getConfig({ clientOutDir, cwd }) {
18
18
  logLevel: env.VOVK_LOG_LEVEL ?? conf.logLevel ?? 'info',
19
19
  prettifyClient: (env.VOVK_PRETTIFY_CLIENT ? !!env.VOVK_PRETTIFY_CLIENT : null) ?? conf.prettifyClient ?? false,
20
20
  devHttps: (env.VOVK_DEV_HTTPS ? !!env.VOVK_DEV_HTTPS : null) ?? conf.devHttps ?? false,
21
+ experimental_clientGenerateTemplateNames: conf.experimental_clientGenerateTemplateNames ?? ['ts', 'compiled'],
21
22
  templates: {
22
23
  service: 'vovk-cli/templates/service.ejs',
23
24
  controller: 'vovk-cli/templates/controller.ejs',
@@ -6,7 +6,7 @@ export default function getProjectInfo({ port: givenPort, clientOutDir, cwd, }?:
6
6
  }): Promise<{
7
7
  cwd: string;
8
8
  port: string;
9
- apiEntryPoint: string;
9
+ apiRoot: string;
10
10
  apiDir: string;
11
11
  srcRoot: string;
12
12
  schemaOutImportPath: string;
@@ -6,7 +6,7 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, cw
6
6
  // Make PORT available to the config file at getConfig
7
7
  process.env.PORT = port;
8
8
  const { config, srcRoot, configAbsolutePaths, userConfig, error } = await getConfig({ clientOutDir, cwd });
9
- const apiEntryPoint = `${config.origin ?? ''}/${config.rootEntry}`;
9
+ const apiRoot = `${config.origin ?? ''}/${config.rootEntry}`;
10
10
  const apiDir = path.join(srcRoot, 'app', config.rootEntry);
11
11
  const schemaOutImportPath = path.relative(config.clientOutDir, config.schemaOutDir).replace(/\\/g, '/'); // windows fix
12
12
  const fetcherClientImportPath = config.fetcher.startsWith('.')
@@ -25,7 +25,7 @@ export default async function getProjectInfo({ port: givenPort, clientOutDir, cw
25
25
  return {
26
26
  cwd,
27
27
  port,
28
- apiEntryPoint,
28
+ apiRoot,
29
29
  apiDir,
30
30
  srcRoot,
31
31
  schemaOutImportPath,
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import { Command } from 'commander';
7
7
  import concurrently from 'concurrently';
8
8
  import getAvailablePort from './utils/getAvailablePort.mjs';
9
9
  import getProjectInfo from './getProjectInfo/index.mjs';
10
- import generateClient from './generateClient.mjs';
10
+ import generate from './generate/index.mjs';
11
11
  import locateSegments from './locateSegments.mjs';
12
12
  import { VovkDev } from './dev/index.mjs';
13
13
  import newComponents from './new/index.mjs';
@@ -19,11 +19,13 @@ initProgram(program.command('init'));
19
19
  program
20
20
  .command('dev')
21
21
  .description('Start schema watcher (optional flag --next-dev to start it with Next.js)')
22
- .option('--next-dev', 'Start schema watcher and Next.js with automatic port allocation', false)
22
+ .option('--next-dev', 'Start schema watcher and Next.js with automatic port allocation')
23
+ .option('--exit', 'Kill the processe when schema and client is generated')
23
24
  .allowUnknownOption(true)
24
25
  .action(async (options, command) => {
26
+ const { nextDev, exit = false } = options;
25
27
  const portAttempts = 30;
26
- const PORT = !options.nextDev
28
+ const PORT = !nextDev
27
29
  ? process.env.PORT
28
30
  : process.env.PORT ||
29
31
  (await getAvailablePort(3000, portAttempts, 0, (failedPort, tryingPort) =>
@@ -34,7 +36,7 @@ program
34
36
  if (!PORT) {
35
37
  throw new Error('🐺 ❌ PORT env variable is required');
36
38
  }
37
- if (options.nextDev) {
39
+ if (nextDev) {
38
40
  const { result } = concurrently([
39
41
  {
40
42
  command: `npx next dev ${command.args.join(' ')}`,
@@ -43,12 +45,17 @@ program
43
45
  },
44
46
  {
45
47
  command: `node ${import.meta.dirname}/dev/index.mjs`,
46
- name: 'Vovk Dev Command',
47
- env: { PORT, __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true' },
48
+ name: 'Vovk Dev Watcher',
49
+ env: {
50
+ PORT,
51
+ __VOVK_START_WATCHER_IN_STANDALONE_MODE__: 'true',
52
+ __VOVK_EXIT__: exit ? 'true' : 'false',
53
+ },
48
54
  },
49
55
  ], {
50
56
  killOthers: ['failure', 'success'],
51
57
  prefix: 'none',
58
+ successCondition: 'first',
52
59
  });
53
60
  try {
54
61
  await result;
@@ -58,7 +65,7 @@ program
58
65
  }
59
66
  }
60
67
  else {
61
- void new VovkDev().start();
68
+ void new VovkDev().start({ exit });
62
69
  }
63
70
  });
64
71
  program
@@ -66,19 +73,18 @@ program
66
73
  .alias('g')
67
74
  .description('Generate client')
68
75
  .option('--out, --client-out-dir <path>', 'Path to output directory')
69
- .option('--template, --templates <templates...>', 'Client code templates')
70
- .option('--no-client', 'Do not generate client')
76
+ .option('--template, --templates <templates...>', 'Client code templates ("ts", "compiled", "python", "none", a custom path)')
71
77
  .option('--full-schema [fileName]', 'Generate client with full schema')
72
78
  .option('--prettify', 'Prettify output files')
73
79
  .action(async (options) => {
74
- const { clientOutDir, templates, prettify, noClient, fullSchema } = options;
80
+ const { clientOutDir, templates, prettify, fullSchema } = options;
75
81
  const projectInfo = await getProjectInfo({ clientOutDir });
76
82
  const { cwd, config, apiDir } = projectInfo;
77
83
  const segments = await locateSegments({ dir: apiDir, config });
78
84
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
79
85
  const schemaImportUrl = pathToFileURL(path.join(schemaOutAbsolutePath, 'index.js')).href;
80
- const { default: segmentsSchema } = await import(schemaImportUrl);
81
- await generateClient({ projectInfo, segments, segmentsSchema, templates, prettify, noClient, fullSchema });
86
+ const { default: segmentsSchema } = (await import(schemaImportUrl));
87
+ await generate({ projectInfo, segments, segmentsSchema, templates, prettify, fullSchema });
82
88
  });
83
89
  program
84
90
  .command('new [components...]')
@@ -5,7 +5,7 @@ import getNPMPackageMetadata from '../utils/getNPMPackageMetadata.mjs';
5
5
  async function updateDeps({ packageJson, packageNames, channel, key, }) {
6
6
  return Promise.all(packageNames.map(async (packageName) => {
7
7
  const metadata = await getNPMPackageMetadata(packageName);
8
- const isVovk = packageName.startsWith('vovk');
8
+ const isVovk = packageName.startsWith('vovk') && packageName !== 'vovk-mapped-types';
9
9
  const latestVersion = metadata['dist-tags'][isVovk ? (channel ?? 'latest') : 'latest'];
10
10
  if (!packageJson[key]) {
11
11
  packageJson[key] = {};
@@ -4,7 +4,7 @@ export type Segment = {
4
4
  segmentName: string;
5
5
  segmentImportPath: string;
6
6
  };
7
- export default function locateSegments({ dir, rootDir, config }: {
7
+ export default function locateSegments({ dir, rootDir, config, }: {
8
8
  dir: string;
9
9
  rootDir?: string;
10
10
  config: Required<VovkConfig> | null;
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import getFileSystemEntryType from './utils/getFileSystemEntryType.mjs';
4
4
  // config: null is used for testing
5
- export default async function locateSegments({ dir, rootDir, config }) {
5
+ export default async function locateSegments({ dir, rootDir, config, }) {
6
6
  let results = [];
7
7
  rootDir = rootDir ?? dir;
8
8
  // Read the contents of the directory
@@ -1,24 +1,21 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- /**
4
- * Checks if a file exists at the given path.
5
- * @param {string} filePath - The path to the file.
6
- * @returns {Promise<boolean>} - A promise that resolves to true if the file exists, false otherwise.
7
- */
8
- const getFileSystemEntryType = async (filePath) => !!(await fs.stat(filePath).catch(() => false));
3
+ import getFileSystemEntryType from './utils/getFileSystemEntryType.mjs';
9
4
  async function postinstall() {
10
- const vovk = path.join(import.meta.dirname, '../../.vovk');
11
- const js = path.join(vovk, 'client.js');
12
- const ts = path.join(vovk, 'client.d.ts');
5
+ // TODO: The function doesn't consider client templates, how to do that?
6
+ const vovk = path.join(import.meta.dirname, '../../.vovk-client');
7
+ const js = path.join(vovk, 'compiled.js');
8
+ const ts = path.join(vovk, 'compiled.d.ts');
13
9
  const index = path.join(vovk, 'index.ts');
14
- if ((await getFileSystemEntryType(js)) ||
15
- (await getFileSystemEntryType(ts)) ||
16
- (await getFileSystemEntryType(index))) {
17
- return;
18
- }
19
10
  await fs.mkdir(vovk, { recursive: true });
20
- await fs.writeFile(js, '/* postinstall */');
21
- await fs.writeFile(ts, '/* postinstall */');
22
- await fs.writeFile(index, '/* postinstall */');
11
+ if (!(await getFileSystemEntryType(js))) {
12
+ await fs.writeFile(js, '/* postinstall */');
13
+ }
14
+ if (!(await getFileSystemEntryType(ts))) {
15
+ await fs.writeFile(ts, '/* postinstall */');
16
+ }
17
+ if (!(await getFileSystemEntryType(index))) {
18
+ await fs.writeFile(index, '/* postinstall */');
19
+ }
23
20
  }
24
21
  void postinstall();
package/dist/types.d.mts CHANGED
@@ -16,6 +16,7 @@ export type VovkEnv = {
16
16
  VOVK_PRETTIFY_CLIENT?: string;
17
17
  VOVK_DEV_HTTPS?: string;
18
18
  __VOVK_START_WATCHER_IN_STANDALONE_MODE__?: 'true';
19
+ __VOVK_EXIT__?: 'true' | 'false';
19
20
  };
20
21
  export type VovkConfig = {
21
22
  clientOutDir?: string;
@@ -30,6 +31,7 @@ export type VovkConfig = {
30
31
  logLevel?: LogLevelNames;
31
32
  prettifyClient?: boolean;
32
33
  devHttps?: boolean;
34
+ experimental_clientGenerateTemplateNames?: string[];
33
35
  templates?: {
34
36
  service?: string;
35
37
  controller?: string;
@@ -46,13 +48,13 @@ export type VovkModuleRenderResult = {
46
48
  };
47
49
  export interface DevOptions {
48
50
  nextDev?: boolean;
51
+ exit?: boolean;
49
52
  }
50
53
  export interface GenerateOptions {
51
54
  clientOutDir?: string;
52
55
  templates?: string[];
53
56
  prettify?: boolean;
54
57
  fullSchema?: string | boolean;
55
- noClient?: boolean;
56
58
  }
57
59
  export interface InitOptions {
58
60
  yes?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.42",
3
+ "version": "0.0.1-draft.44",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -36,27 +36,27 @@
36
36
  },
37
37
  "homepage": "https://vovk.dev",
38
38
  "peerDependencies": {
39
- "vovk": "^3.0.0-draft.34"
39
+ "vovk": "^3.0.0-draft.50"
40
40
  },
41
41
  "dependencies": {
42
- "@inquirer/prompts": "^7.1.0",
43
- "@npmcli/package-json": "^6.1.0",
44
- "chalk": "^5.3.0",
45
- "chokidar": "^4.0.1",
46
- "commander": "^12.1.0",
47
- "concurrently": "^9.1.0",
48
- "dotenv": "^16.4.5",
42
+ "@inquirer/prompts": "^7.2.3",
43
+ "@npmcli/package-json": "^6.1.1",
44
+ "chalk": "^5.4.1",
45
+ "chokidar": "^4.0.3",
46
+ "commander": "^13.1.0",
47
+ "concurrently": "^9.1.2",
48
+ "dotenv": "^16.4.7",
49
49
  "ejs": "^3.1.10",
50
50
  "gray-matter": "^4.0.3",
51
- "inflection": "^3.0.0",
51
+ "inflection": "^3.0.2",
52
52
  "jsonc-parser": "^3.3.1",
53
53
  "lodash": "^4.17.21",
54
54
  "loglevel": "^1.9.2",
55
55
  "pluralize": "^8.0.0",
56
- "prettier": "^3.4.1",
56
+ "prettier": "^3.4.2",
57
57
  "tar-stream": "^3.1.7",
58
- "ts-morph": "^24.0.0",
59
- "undici": "^7.0.0"
58
+ "ts-morph": "^25.0.0",
59
+ "undici": "^7.3.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@types/concat-stream": "^2.0.3",
@@ -65,8 +65,8 @@
65
65
  "@types/pluralize": "^0.0.33",
66
66
  "@types/tar-stream": "^3.1.3",
67
67
  "concat-stream": "^2.0.0",
68
- "create-next-app": "^15.0.3",
68
+ "create-next-app": "^15.1.6",
69
69
  "node-pty": "^1.0.0",
70
- "type-fest": "^4.29.0"
70
+ "type-fest": "^4.33.0"
71
71
  }
72
72
  }
@@ -1,12 +0,0 @@
1
- import type { VovkSchema } from 'vovk';
2
- import type { ProjectInfo } from './getProjectInfo/index.mjs';
3
- import type { Segment } from './locateSegments.mjs';
4
- import { GenerateOptions } from './types.mjs';
5
- export default function generateClient({ projectInfo, segments, segmentsSchema, templates, prettify: prettifyClient, fullSchema, noClient, }: {
6
- projectInfo: ProjectInfo;
7
- segments: Segment[];
8
- segmentsSchema: Record<string, VovkSchema>;
9
- } & Pick<GenerateOptions, 'templates' | 'prettify' | 'fullSchema' | 'noClient'>): Promise<{
10
- written: boolean;
11
- path: string;
12
- }>;
@@ -1,91 +0,0 @@
1
- import path from 'node:path';
2
- import fs from 'node:fs/promises';
3
- import ejs from 'ejs';
4
- import formatLoggedSegmentName from './utils/formatLoggedSegmentName.mjs';
5
- import prettify from './utils/prettify.mjs';
6
- export default async function generateClient({ projectInfo, segments, segmentsSchema, templates = ['ts', 'compiled'], prettify: prettifyClient, fullSchema, noClient, }) {
7
- const { config, cwd, log, validateOnClientImportPath, apiEntryPoint, fetcherClientImportPath, schemaOutImportPath, } = projectInfo;
8
- const __dirname = path.dirname(new URL(import.meta.url).pathname);
9
- const templatesDir = path.join(__dirname, '..', 'client-templates');
10
- const clientOutDirAbsolutePath = path.resolve(cwd, config.clientOutDir);
11
- const mapper = (dir) => (name) => ({
12
- templatePath: path.resolve(templatesDir, dir, name),
13
- outPath: path.join(clientOutDirAbsolutePath, name.replace('.ejs', '')),
14
- });
15
- const builtInTemplatesMap = {
16
- ts: ['index.ts.ejs'].map(mapper('ts')),
17
- compiled: ['client.js.ejs', 'client.d.ts.ejs'].map(mapper('compiled')),
18
- python: ['__init__.py'].map(mapper('python')),
19
- };
20
- const templateFiles = templates.reduce((acc, template) => {
21
- if (template in builtInTemplatesMap) {
22
- return [...acc, ...builtInTemplatesMap[template]];
23
- }
24
- return [...acc, {
25
- templatePath: path.resolve(cwd, template),
26
- outPath: path.join(clientOutDirAbsolutePath, path.basename(template).replace('.ejs', ''))
27
- }];
28
- }, []);
29
- // Ensure that each segment has a matching schema if it needs to be emitted:
30
- for (let i = 0; i < segments.length; i++) {
31
- const { segmentName } = segments[i];
32
- const schema = segmentsSchema[segmentName];
33
- if (!schema) {
34
- throw new Error(`Unable to generate client. No schema found for ${formatLoggedSegmentName(segmentName)}`);
35
- }
36
- if (!schema.emitSchema)
37
- continue;
38
- }
39
- const now = Date.now();
40
- // Data for the EJS templates:
41
- const ejsData = {
42
- apiEntryPoint,
43
- fetcherClientImportPath,
44
- schemaOutImportPath,
45
- validateOnClientImportPath,
46
- segments,
47
- segmentsSchema,
48
- };
49
- // 1. Process each template in parallel
50
- const processedTemplates = noClient ? [] : await Promise.all(templateFiles.map(async ({ templatePath, outPath }) => {
51
- // Read the EJS template
52
- const templateContent = await fs.readFile(templatePath, 'utf-8');
53
- // Render the template
54
- let rendered = templatePath.endsWith('.ejs') ? ejs.render(templateContent, ejsData) : templateContent;
55
- // Optionally prettify
56
- if (prettifyClient || config.prettifyClient) {
57
- rendered = await prettify(rendered, outPath);
58
- }
59
- // Read existing file content to compare
60
- const existingContent = await fs.readFile(outPath, 'utf-8').catch(() => '');
61
- // Determine if we need to rewrite the file
62
- const needsWriting = existingContent !== rendered;
63
- return {
64
- outPath,
65
- rendered,
66
- needsWriting,
67
- };
68
- }));
69
- if (fullSchema) {
70
- const fullSchemaOutAbsolutePath = path.resolve(clientOutDirAbsolutePath, typeof fullSchema === 'string' ? fullSchema : 'full-schema.json');
71
- await fs.writeFile(fullSchemaOutAbsolutePath, JSON.stringify(segmentsSchema, null, 2));
72
- log.info(`Full schema written to ${fullSchemaOutAbsolutePath}`);
73
- }
74
- // 2. Check if any file needs rewriting
75
- const anyNeedsWriting = processedTemplates.some(({ needsWriting }) => needsWriting);
76
- if (!anyNeedsWriting) {
77
- log.debug(`Client is up to date and doesn't need to be regenerated (${Date.now() - now}ms)`);
78
- return { written: false, path: clientOutDirAbsolutePath };
79
- }
80
- // 3. Make sure the output directory exists
81
- await fs.mkdir(clientOutDirAbsolutePath, { recursive: true });
82
- // 4. Write updated files where needed
83
- await Promise.all(processedTemplates.map(({ outPath, rendered, needsWriting }) => {
84
- if (needsWriting) {
85
- return fs.writeFile(outPath, rendered);
86
- }
87
- return null;
88
- }));
89
- log.info(`Client generated in ${Date.now() - now}ms`);
90
- return { written: true, path: clientOutDirAbsolutePath };
91
- }