vovk-cli 0.0.1-draft.37 → 0.0.1-draft.39

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.
@@ -22,7 +22,7 @@ export class VovkDev {
22
22
  #isWatching = false;
23
23
  #modulesWatcher = null;
24
24
  #segmentWatcher = null;
25
- #watchSegments = () => {
25
+ #watchSegments = (callback) => {
26
26
  const segmentReg = /\/?\[\[\.\.\.[a-zA-Z-_]+\]\]\/route.ts$/;
27
27
  const { cwd, log, config, apiDir } = this.#projectInfo;
28
28
  const schemaOutAbsolutePath = path.join(cwd, config.schemaOutDir);
@@ -77,13 +77,14 @@ export class VovkDev {
77
77
  }
78
78
  })
79
79
  .on('ready', () => {
80
+ callback();
80
81
  log.debug('Segments watcher is ready');
81
82
  })
82
83
  .on('error', (error) => {
83
84
  log.error(`Error watching segments folder: ${error?.message ?? 'Unknown error'}`);
84
85
  });
85
86
  };
86
- #watchModules = () => {
87
+ #watchModules = (callback) => {
87
88
  const { config, cwd, log } = this.#projectInfo;
88
89
  const modulesDirAbsolutePath = path.join(cwd, config.modulesDir);
89
90
  log.debug(`Watching modules at ${modulesDirAbsolutePath}`);
@@ -115,30 +116,37 @@ export class VovkDev {
115
116
  }
116
117
  })
117
118
  .on('ready', () => {
119
+ callback();
118
120
  log.debug('Modules watcher is ready');
119
121
  })
120
122
  .on('error', (error) => {
121
123
  log.error(`Error watching modules folder: ${error?.message ?? 'Unknown error'}`);
122
124
  });
123
125
  };
124
- #watchConfig = () => {
126
+ #watchConfig = (callback) => {
125
127
  const { log, cwd } = this.#projectInfo;
126
128
  log.debug(`Watching config files`);
127
129
  let isInitial = true;
128
130
  let isReady = false;
129
131
  const handle = debounce(async () => {
130
132
  this.#projectInfo = await getProjectInfo();
131
- if (!isInitial) {
133
+ await this.#modulesWatcher?.close();
134
+ await this.#segmentWatcher?.close();
135
+ await Promise.all([
136
+ new Promise((resolve) => this.#watchModules(() => resolve(0))),
137
+ new Promise((resolve) => this.#watchSegments(() => resolve(0)))
138
+ ]);
139
+ if (isInitial) {
140
+ callback();
141
+ }
142
+ else {
132
143
  log.info('Config file has been updated');
133
144
  }
134
145
  isInitial = false;
135
- await this.#modulesWatcher?.close();
136
- await this.#segmentWatcher?.close();
137
- this.#watchModules();
138
- this.#watchSegments();
139
146
  }, 1000);
140
147
  chokidar
141
- .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
148
+ // .watch(['vovk.config.{js,mjs,cjs}', '.config/vovk.config.{js,mjs,cjs}'], {
149
+ .watch(['vovk.config.js', 'vovk.config.mjs', 'vovk.config.cjs', '.config/vovk.config.js', '.config/vovk.config.mjs', '.config/vovk.config.cjs'], {
142
150
  persistent: true,
143
151
  cwd,
144
152
  ignoreInitial: false,
@@ -157,15 +165,14 @@ export class VovkDev {
157
165
  .on('error', (error) => log.error(`Error watching config files: ${error?.message ?? 'Unknown error'}`));
158
166
  void handle();
159
167
  };
160
- async #watch() {
168
+ async #watch(callback) {
161
169
  if (this.#isWatching)
162
170
  throw new Error('Already watching');
163
171
  const { log } = this.#projectInfo;
164
172
  log.debug(`Starting segments and modules watcher. Detected initial segments: ${JSON.stringify(this.#segments.map((s) => s.segmentName))}.`);
165
173
  await ensureClient(this.#projectInfo);
166
174
  // automatically watches segments and modules
167
- this.#watchConfig();
168
- log.info('Ready');
175
+ this.#watchConfig(callback);
169
176
  }
170
177
  #processControllerChange = async (filePath) => {
171
178
  const { log } = this.#projectInfo;
@@ -208,22 +215,29 @@ export class VovkDev {
208
215
  const { devHttps } = config;
209
216
  const endpoint = `${apiEntryPoint.startsWith(`http${devHttps ? 's' : ''}://`) ? apiEntryPoint : `http${devHttps ? 's' : ''}://localhost:${port}${apiEntryPoint}`}/${segmentName ? `${segmentName}/` : ''}_schema_`;
210
217
  log.debug(`Requesting schema for ${formatLoggedSegmentName(segmentName)} at ${endpoint}`);
211
- const resp = await fetch(endpoint);
212
- if (resp.status !== 200) {
213
- const probableCause = {
214
- 404: 'The segment did not compile or config.origin is wrong.',
215
- }[resp.status];
216
- log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
217
- return;
218
- }
219
- let schema = null;
220
218
  try {
221
- ({ schema } = (await resp.json()));
219
+ const resp = await fetch(endpoint);
220
+ if (resp.status !== 200) {
221
+ const probableCause = {
222
+ 404: 'The segment did not compile or config.origin is wrong.',
223
+ }[resp.status];
224
+ log.warn(`Schema request to ${formatLoggedSegmentName(segmentName)} failed with status code ${resp.status} but expected 200.${probableCause ? ` Probable cause: ${probableCause}` : ''}`);
225
+ return { isError: true };
226
+ }
227
+ let schema = null;
228
+ try {
229
+ ({ schema } = (await resp.json()));
230
+ }
231
+ catch (error) {
232
+ log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
233
+ }
234
+ await this.#handleSchema(schema);
222
235
  }
223
236
  catch (error) {
224
- log.error(`Error parsing schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
237
+ log.error(`Error requesting schema for ${formatLoggedSegmentName(segmentName)}: ${error.message}`);
238
+ return { isError: true };
225
239
  }
226
- await this.#handleSchema(schema);
240
+ return { isError: false };
227
241
  }, 500);
228
242
  async #handleSchema(schema) {
229
243
  const { log, config, cwd } = this.#projectInfo;
@@ -261,8 +275,10 @@ export class VovkDev {
261
275
  }
262
276
  }
263
277
  async start() {
278
+ const now = Date.now();
264
279
  this.#projectInfo = await getProjectInfo();
265
280
  const { log, config, cwd, apiDir } = this.#projectInfo;
281
+ log.info('Starting');
266
282
  if (config.devHttps) {
267
283
  const agent = new Agent({
268
284
  connect: {
@@ -284,9 +300,29 @@ export class VovkDev {
284
300
  // Request schema every segment in 5 seconds in order to update schema and start watching
285
301
  setTimeout(() => {
286
302
  for (const { segmentName } of this.#segments) {
287
- void this.#requestSchema(segmentName);
303
+ const MAX_ATTEMPTS = 3;
304
+ let attempts = 0;
305
+ void this.#requestSchema(segmentName).then(({ isError }) => {
306
+ if (isError) {
307
+ const interval = setInterval(() => {
308
+ attempts++;
309
+ if (attempts >= MAX_ATTEMPTS) {
310
+ clearInterval(interval);
311
+ log.error(`Failed to request schema for ${formatLoggedSegmentName(segmentName)} after ${MAX_ATTEMPTS} attempts`);
312
+ return;
313
+ }
314
+ void this.#requestSchema(segmentName).then(({ isError: isError2 }) => {
315
+ if (!isError2) {
316
+ clearInterval(interval);
317
+ }
318
+ });
319
+ }, 5000);
320
+ }
321
+ });
288
322
  }
289
- this.#watch();
323
+ this.#watch(() => {
324
+ log.info(`Ready in ${Date.now() - now}ms`);
325
+ });
290
326
  }, 5000);
291
327
  }
292
328
  }
@@ -7,16 +7,17 @@ import getFileSystemEntryType from '../utils/getFileSystemEntryType.mjs';
7
7
  import installDependencies, { getPackageManager } from './installDependencies.mjs';
8
8
  import getLogger from '../utils/getLogger.mjs';
9
9
  import createConfig from './createConfig.mjs';
10
- import updateNPMScripts from './updateNPMScripts.mjs';
10
+ import updateNPMScripts, { getDevScript } from './updateNPMScripts.mjs';
11
11
  import checkTSConfigForExperimentalDecorators from './checkTSConfigForExperimentalDecorators.mjs';
12
12
  import updateTypeScriptConfig from './updateTypeScriptConfig.mjs';
13
13
  import updateDependenciesWithoutInstalling from './updateDependenciesWithoutInstalling.mjs';
14
14
  import logUpdateDependenciesError from './logUpdateDependenciesError.mjs';
15
15
  import chalkHighlightThing from '../utils/chalkHighlightThing.mjs';
16
+ import NPMCliPackageJson from '@npmcli/package-json';
16
17
  export class Init {
17
18
  root;
18
19
  log;
19
- async #init({ configPaths, }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, validateOnClient, dryRun, channel, }) {
20
+ async #init({ configPaths, pkgJson, }, { useNpm, useYarn, usePnpm, useBun, skipInstall, updateTsConfig, updateScripts, validationLibrary, validateOnClient, dryRun, channel, }) {
20
21
  const { log, root } = this;
21
22
  const dependencies = ['vovk', 'vovk-client'];
22
23
  const devDependencies = ['vovk-cli'];
@@ -30,13 +31,13 @@ export class Init {
30
31
  dependencies.push(...({
31
32
  'vovk-zod': ['zod'],
32
33
  'vovk-yup': ['yup'],
33
- 'vovk-dto': ['class-validator', 'class-transformer'],
34
+ 'vovk-dto': ['class-validator', 'class-transformer', 'vovk-mapped-types', 'reflect-metadata'],
34
35
  }[validationLibrary] ?? []));
35
36
  }
36
37
  if (updateScripts) {
37
38
  try {
38
39
  if (!dryRun)
39
- await updateNPMScripts(root, updateScripts);
40
+ await updateNPMScripts(pkgJson, root, updateScripts);
40
41
  log.info('Updated scripts at package.json');
41
42
  }
42
43
  catch (error) {
@@ -113,11 +114,12 @@ export class Init {
113
114
  const cwd = process.cwd();
114
115
  const root = path.resolve(cwd, prefix);
115
116
  const log = getLogger(logLevel);
117
+ const pkgJson = await NPMCliPackageJson.load(root);
116
118
  this.root = root;
117
119
  this.log = log;
118
120
  const configPaths = await getConfigPaths({ cwd, relativePath: prefix });
119
121
  if (yes) {
120
- return this.#init({ configPaths }, {
122
+ return this.#init({ configPaths, pkgJson }, {
121
123
  useNpm: useNpm ?? (!useYarn && !usePnpm && !useBun),
122
124
  useYarn: useYarn ?? false,
123
125
  usePnpm: usePnpm ?? false,
@@ -164,7 +166,7 @@ export class Init {
164
166
  {
165
167
  name: 'vovk-dto',
166
168
  value: 'vovk-dto',
167
- description: 'Use class-validator and class-transformer for data validation',
169
+ description: 'Use class-validator for data validation. Also installs class-transformer, vovk-mapped-types and reflect-metadata',
168
170
  },
169
171
  { name: 'None', value: null, description: 'Install validation library later' },
170
172
  ],
@@ -185,12 +187,12 @@ export class Init {
185
187
  {
186
188
  name: 'Yes, use "concurrently" implicitly',
187
189
  value: 'implicit',
188
- description: `The "dev" script will use "concurrently" API to run "next dev" and "vovk dev" commands together and automatically find an available port ${chalk.whiteBright.bold(`"vovk dev --next-dev"`)}`,
190
+ description: `The "dev" script will use "concurrently" API to run "next dev" and "vovk dev" commands together and automatically find an available port ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'implicit')}"`)}`,
189
191
  },
190
192
  {
191
193
  name: 'Yes, use "concurrently" explicitly',
192
194
  value: 'explicit',
193
- description: `The "dev" script will use pre-defined PORT variable and run "next dev" and "vovk dev" as "concurrently" CLI arguments ${chalk.whiteBright.bold(`"PORT=3000 concurrently 'next dev' 'vovk dev' --kill-others"`)}`,
195
+ description: `The "dev" script will use pre-defined PORT variable and run "next dev" and "vovk dev" as "concurrently" CLI arguments ${chalk.whiteBright.bold(`"${getDevScript(pkgJson, 'explicit')}"`)}`,
194
196
  },
195
197
  {
196
198
  name: 'No',
@@ -213,7 +215,7 @@ export class Init {
213
215
  });
214
216
  }
215
217
  }
216
- await this.#init({ configPaths }, {
218
+ await this.#init({ configPaths, pkgJson }, {
217
219
  useNpm: useNpm ?? (!useYarn && !usePnpm && !useBun),
218
220
  useYarn: useYarn ?? false,
219
221
  usePnpm: usePnpm ?? false,
@@ -1 +1,3 @@
1
- export default function updateNPMScripts(root: string, updateScriptsMode: 'implicit' | 'explicit'): Promise<void>;
1
+ import NPMCliPackageJson from '@npmcli/package-json';
2
+ export declare function getDevScript(pkgJson: NPMCliPackageJson, updateScriptsMode: 'implicit' | 'explicit'): string;
3
+ export default function updateNPMScripts(pkgJson: NPMCliPackageJson, root: string, updateScriptsMode: 'implicit' | 'explicit'): Promise<void>;
@@ -1,13 +1,16 @@
1
- import NPMCliPackageJson from '@npmcli/package-json';
2
- export default async function updateNPMScripts(root, updateScriptsMode) {
3
- const pkgJson = await NPMCliPackageJson.load(root);
1
+ export function getDevScript(pkgJson, updateScriptsMode) {
2
+ const nextDev = pkgJson.content.scripts?.dev ?? 'next dev';
3
+ const nextDevFlags = nextDev.replace('next dev', '').trim();
4
+ return updateScriptsMode === 'explicit'
5
+ ? `PORT=3000 concurrently '${nextDev}' 'vovk dev' --kill-others`
6
+ : `vovk dev --next-dev${nextDevFlags ? ` -- ${nextDevFlags}` : ''}`;
7
+ }
8
+ export default async function updateNPMScripts(pkgJson, root, updateScriptsMode) {
4
9
  pkgJson.update({
5
10
  scripts: {
6
11
  ...pkgJson.content.scripts,
7
12
  generate: 'vovk generate',
8
- dev: updateScriptsMode === 'explicit'
9
- ? "PORT=3000 concurrently 'next dev' 'vovk dev' --kill-others"
10
- : 'vovk dev --next-dev',
13
+ dev: getDevScript(pkgJson, updateScriptsMode),
11
14
  },
12
15
  });
13
16
  await pkgJson.save();
@@ -29,5 +29,5 @@ ${segmentName ? ` segmentName: '${segmentName}',\n` : ''} emitSchema: true,
29
29
  await fs.mkdir(path.dirname(absoluteSegmentRoutePath), { recursive: true });
30
30
  await fs.writeFile(absoluteSegmentRoutePath, code);
31
31
  }
32
- log.info(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} created at ${absoluteSegmentRoutePath}. Run ${chalkHighlightThing(`vovk new controller ${[segmentName, 'someName'].filter(Boolean).join('/')}`)} to create a new controller`);
32
+ log.info(`${formatLoggedSegmentName(segmentName, { upperFirst: true })} created at ${absoluteSegmentRoutePath}. Run ${chalkHighlightThing(`vovk new controller ${[segmentName, 'thing'].filter(Boolean).join('/')}`)} to create a new controller`);
33
33
  }
@@ -1,2 +1,2 @@
1
1
  import { KnownAny } from '../types.mjs';
2
- export default function debounceWithArgs<T extends (...args: KnownAny[]) => KnownAny>(fn: T, wait: number): (...args: Parameters<T>) => void | Promise<void>;
2
+ export default function debounceWithArgs<Callback extends (...args: KnownAny[]) => KnownAny>(callback: Callback, wait: number): (...args: Parameters<Callback>) => Promise<Awaited<ReturnType<Callback>>>;
@@ -1,11 +1,29 @@
1
- import debounce from 'lodash/debounce.js';
2
- export default function debounceWithArgs(fn, wait) {
3
- const debouncedFunctions = new Map();
1
+ export default function debounceWithArgs(callback, wait) {
2
+ // Stores timeouts keyed by the stringified arguments
3
+ const timeouts = new Map();
4
4
  return (...args) => {
5
+ // Convert arguments to a JSON string (or any other stable key generation)
5
6
  const key = JSON.stringify(args);
6
- if (!debouncedFunctions.has(key)) {
7
- debouncedFunctions.set(key, debounce(fn, wait));
7
+ // Clear any existing timer for this specific key
8
+ if (timeouts.has(key)) {
9
+ clearTimeout(timeouts.get(key));
8
10
  }
9
- return debouncedFunctions.get(key)(...args);
11
+ // Return a promise that resolves/rejects after the debounce delay
12
+ return new Promise((resolve, reject) => {
13
+ const timeoutId = setTimeout(async () => {
14
+ try {
15
+ const result = await callback(...args);
16
+ resolve(result);
17
+ }
18
+ catch (error) {
19
+ reject(error);
20
+ }
21
+ finally {
22
+ // Remove the entry once the callback is invoked
23
+ timeouts.delete(key);
24
+ }
25
+ }, wait);
26
+ timeouts.set(key, timeoutId);
27
+ });
10
28
  };
11
29
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vovk-cli",
3
- "version": "0.0.1-draft.37",
3
+ "version": "0.0.1-draft.39",
4
4
  "bin": {
5
5
  "vovk": "./dist/index.mjs"
6
6
  },
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "homepage": "https://vovk.dev",
38
38
  "peerDependencies": {
39
- "vovk": "^3.0.0-draft.33"
39
+ "vovk": "^3.0.0-draft.34"
40
40
  },
41
41
  "dependencies": {
42
42
  "@inquirer/prompts": "^7.1.0",