vovk-cli 0.2.0 → 0.3.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/client-templates/schema-ts/schema.ts.ejs +1 -1
  3. package/client-templates/ts-base/index.ts.ejs +1 -1
  4. package/dist/bundle/index.mjs +64 -42
  5. package/dist/dev/diff-segment-schema.d.mts +0 -17
  6. package/dist/dev/diff-segment-schema.mjs +1 -17
  7. package/dist/dev/ensure-schema-files.mjs +28 -10
  8. package/dist/dev/index.mjs +19 -13
  9. package/dist/dev/log-diff-result.d.mts +1 -1
  10. package/dist/dev/log-diff-result.mjs +1 -1
  11. package/dist/dev/write-meta-json.mjs +2 -2
  12. package/dist/dev/write-one-segment-schema-file.mjs +7 -2
  13. package/dist/generate/ensure-client.mjs +1 -1
  14. package/dist/generate/generate.mjs +38 -25
  15. package/dist/generate/get-client-template-files.mjs +8 -4
  16. package/dist/generate/get-project-full-schema.d.mts +1 -1
  17. package/dist/generate/get-project-full-schema.mjs +2 -2
  18. package/dist/generate/index.d.mts +2 -2
  19. package/dist/generate/index.mjs +46 -34
  20. package/dist/generate/write-one-client-file.d.mts +3 -3
  21. package/dist/generate/write-one-client-file.mjs +8 -6
  22. package/dist/get-project-info/get-config/get-relative-src-root.mjs +1 -1
  23. package/dist/get-project-info/get-config/get-template-defs.d.mts +20 -23
  24. package/dist/get-project-info/get-config/get-template-defs.mjs +21 -47
  25. package/dist/get-project-info/get-config/get-user-config.mjs +1 -1
  26. package/dist/get-project-info/get-config/index.d.mts +1 -1
  27. package/dist/get-project-info/get-config/index.mjs +15 -10
  28. package/dist/get-project-info/get-meta-schema.d.mts +1 -1
  29. package/dist/get-project-info/get-meta-schema.mjs +1 -1
  30. package/dist/get-project-info/index.mjs +2 -2
  31. package/dist/index.mjs +25 -16
  32. package/dist/init/check-tsconfig-for-experimental-decorators.mjs +1 -1
  33. package/dist/init/create-config.d.mts +1 -1
  34. package/dist/init/create-config.mjs +5 -5
  35. package/dist/init/index.d.mts +1 -1
  36. package/dist/init/index.mjs +49 -17
  37. package/dist/init/install-dependencies.d.mts +2 -1
  38. package/dist/init/install-dependencies.mjs +10 -2
  39. package/dist/init/update-dependencies-without-installing.d.mts +1 -1
  40. package/dist/init/update-dependencies-without-installing.mjs +0 -1
  41. package/dist/init/update-gitignore.d.mts +2 -0
  42. package/dist/init/update-gitignore.mjs +20 -0
  43. package/dist/init/update-typescript-config.mjs +1 -1
  44. package/dist/new/index.d.mts +1 -1
  45. package/dist/new/new-module.mjs +11 -16
  46. package/dist/new/new-segment.mjs +9 -4
  47. package/dist/new/render.mjs +1 -1
  48. package/dist/types.d.mts +1 -1
  49. package/dist/utils/compile-json-schema-to-typescript-type.d.mts +1 -1
  50. package/dist/utils/compile-ts.mjs +1 -1
  51. package/dist/utils/generate-fn-name.d.mts +0 -9
  52. package/dist/utils/generate-fn-name.mjs +2 -9
  53. package/dist/utils/generated-banner.d.mts +1 -0
  54. package/dist/utils/generated-banner.mjs +3 -0
  55. package/dist/utils/get-available-port.d.mts +0 -8
  56. package/dist/utils/get-available-port.mjs +2 -14
  57. package/dist/utils/get-file-system-entry-type.d.mts +5 -4
  58. package/dist/utils/get-file-system-entry-type.mjs +5 -5
  59. package/dist/utils/normalize-openapi-mixin.d.mts +1 -1
  60. package/dist/utils/normalize-openapi-mixin.mjs +18 -3
  61. package/dist/utils/prettify.d.mts +4 -0
  62. package/dist/utils/prettify.mjs +39 -3
  63. package/dist/utils/remove-unlisted-directories.d.mts +1 -9
  64. package/dist/utils/remove-unlisted-directories.mjs +60 -21
  65. package/dist/utils/resolve-absolute-module-path.mjs +1 -2
  66. package/dist/utils/update-config-property.mjs +1 -1
  67. package/package.json +23 -30
  68. package/client-templates/js-base/index.d.ts.ejs +0 -21
  69. package/client-templates/js-base/index.js.ejs +0 -18
  70. package/client-templates/openapi-js/openapi.d.ts.ejs +0 -4
  71. package/client-templates/openapi-js/openapi.js.ejs +0 -4
  72. package/client-templates/schema-js/schema.d.ts.ejs +0 -10
  73. package/client-templates/schema-js/schema.js.ejs +0 -29
package/CHANGELOG.md ADDED
@@ -0,0 +1,38 @@
1
+ # Changelog
2
+
3
+ All notable changes to `vovk-cli` are documented here. This file is the canonical record: noteworthy changes only, one short line each, linked to the pull request or commit that made them. GitHub Releases are not used.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## 0.3.0-beta.0 - unreleased
8
+
9
+ A cleanup release. `vovk-cli` is pre-1.0, so this minor carries breaking changes: `^0.2.0` does not match `0.3.0`.
10
+
11
+ ### Removed
12
+
13
+ - The `vovk-client` package: the composed client generates into your source tree (`src/client`, or `client/` without a `src` folder) and is imported as `@/client`, so it survives `npm ci` and works under pnpm and Yarn PnP ([#29](https://github.com/finom/vovk/pull/29))
14
+ - The `js` template family (`js`, `jsBase`, `schemaJs`, `openapiJs`): use `ts` ([#29](https://github.com/finom/vovk/pull/29))
15
+ - The `prettier` dependency: prettifying resolves prettier from your project and warns once when it is missing ([#29](https://github.com/finom/vovk/pull/29))
16
+ - Unused runtime deps `clone-deep`, `inflection` and `tar-stream` ([25a2f20](https://github.com/finom/vovk/commit/25a2f203)), the pinned linux rolldown binding ([d0cae19](https://github.com/finom/vovk/commit/d0cae19c)), and the pre-kebab collision guard in `vovk new` ([b7dc4b2](https://github.com/finom/vovk/commit/b7dc4b2a))
17
+
18
+ ### Changed
19
+
20
+ - Composed client defaults: `fromTemplates: ['ts']`, `outDir: src/client`, `prettifyClient: true`; `vovk init` gitignores the client dir and no longer installs `vovk-client` ([#29](https://github.com/finom/vovk/pull/29))
21
+ - Broken input fails fast, and the CLI reports what it actually did ([0ad9872](https://github.com/finom/vovk/commit/0ad9872e))
22
+
23
+ ### Fixed
24
+
25
+ - The pruner only deletes files carrying the generated banner, so user files named like generated ones survive ([e9f269c](https://github.com/finom/vovk/commit/e9f269ce), [88065d0](https://github.com/finom/vovk/commit/88065d0c))
26
+ - Schema output cleanup only deletes JSON carrying vovk's own `$schema` id ([9816dda](https://github.com/finom/vovk/commit/9816dda4))
27
+ - A fetched schema's `segmentName` is no longer trusted as a write path ([d93a240](https://github.com/finom/vovk/commit/d93a240c))
28
+ - `vovk init` only spawns a known package manager, never a path from `packageManager` ([053cb1a](https://github.com/finom/vovk/commit/053cb1a8))
29
+ - `--prebundle-out-dir` takes effect and stays inside the project ([22f505c](https://github.com/finom/vovk/commit/22f505c6))
30
+ - `vovk new segment` scaffolds `force-static` for static segments ([#25](https://github.com/finom/vovk/pull/25))
31
+
32
+ ### Upgrading from 0.2.x
33
+
34
+ 1. Remove `vovk-client` from your dependencies and import from `@/client` (or a relative path to the configured `outDir`).
35
+ 2. Replace `fromTemplates: ['js']` and friends with `['ts']`.
36
+ 3. Install `prettier` yourself if you want the client formatted, or set `prettifyClient: false`.
37
+ 4. Add the composed client output directory to `.gitignore`; `vovk init` does this for new projects.
38
+ 5. A stale `node_modules/.vovk-client` is inert and can be deleted.
@@ -24,5 +24,5 @@ export const schema = {
24
24
  meta,
25
25
  };
26
26
  <% if (t.hasMixins) { %>
27
- export { Mixins };
27
+ export type { Mixins };
28
28
  <% } %>
@@ -23,5 +23,5 @@ export const <%= rpcModuleName %> = createRPC<<%= segment.segmentType === 'mixin
23
23
  }) %>
24
24
  export { schema };
25
25
  <% if (t.hasMixins) { %>
26
- export { Mixins };
26
+ export type { Mixins };
27
27
  <% } %>
@@ -1,5 +1,5 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
3
  import groupBy from 'lodash/groupBy.js';
4
4
  import { generate } from '../generate/generate.mjs';
5
5
  import { BuiltInTemplateName } from '../get-project-info/get-config/get-template-defs.mjs';
@@ -9,46 +9,32 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
9
9
  const { config, log, cwd, apiDirAbsolutePath } = projectInfo;
10
10
  const locatedSegments = await locateSegments({ dir: apiDirAbsolutePath, config, log });
11
11
  const { bundle: bundleConfig } = config;
12
+ if (bundleConfig.build.isMissingBuild) {
13
+ throw new Error('No bundle.build function specified in the config. See https://vovk.dev/bundle for details.');
14
+ }
12
15
  const keepPrebundleDir = cliBundleOptions?.keepPrebundleDir ?? bundleConfig?.keepPrebundleDir ?? false;
13
- const prebundleOutDirAbsolute = path.resolve(cwd, cliBundleOptions?.prebundleOutDir ?? bundleConfig.prebundleOutDir);
16
+ const prebundleOutDir = cliBundleOptions?.prebundleOutDir ?? bundleConfig.prebundleOutDir;
17
+ const prebundleOutDirAbsolute = path.resolve(cwd, prebundleOutDir);
18
+ // this directory gets removed recursively, so it must be a directory of our own inside the project
19
+ const relativeToCwd = path.relative(cwd, prebundleOutDirAbsolute);
20
+ const escapesCwd = relativeToCwd === '..' || relativeToCwd.startsWith(`..${path.sep}`);
21
+ if (!relativeToCwd || escapesCwd || path.isAbsolute(relativeToCwd)) {
22
+ throw new Error(`Invalid prebundle output directory ${JSON.stringify(prebundleOutDir)}. It is deleted after bundling, so it must be a subdirectory of the project, such as "tmp_prebundle".`);
23
+ }
14
24
  const entry = path.join(prebundleOutDirAbsolute, 'index.ts');
15
25
  const outDir = cliBundleOptions?.outDir ?? bundleConfig.outDir;
16
26
  if (!outDir) {
17
27
  throw new Error('No output directory specified for bundling');
18
28
  }
29
+ // CLI options win as a pair so config exclude cannot conflict with CLI include
30
+ const [includeSegments, excludeSegments] = cliBundleOptions.includeSegments?.length || cliBundleOptions.excludeSegments?.length
31
+ ? [cliBundleOptions.includeSegments, cliBundleOptions.excludeSegments]
32
+ : [bundleConfig.includeSegments, bundleConfig.excludeSegments];
33
+ if (includeSegments?.length && excludeSegments?.length) {
34
+ throw new Error('Both includeSegments and excludeSegments are set for the bundle. Please use only one of them.');
35
+ }
19
36
  const outDirAbsolute = path.resolve(cwd, outDir);
20
- await generate({
21
- isEnsuringClient: false,
22
- isBundle: true,
23
- projectInfo,
24
- forceNothingWrittenLog: true,
25
- fullSchema,
26
- locatedSegments,
27
- cliGenerateOptions: {
28
- schemaPath: cliBundleOptions?.schemaPath,
29
- origin: cliBundleOptions?.origin,
30
- openapiSpec: cliBundleOptions?.openapiSpec,
31
- openapiGetModuleName: cliBundleOptions?.openapiGetModuleName,
32
- openapiGetMethodName: cliBundleOptions?.openapiGetMethodName,
33
- openapiRootUrl: cliBundleOptions?.openapiRootUrl,
34
- openapiMixinName: cliBundleOptions?.openapiMixinName,
35
- openapiFallback: cliBundleOptions?.openapiFallback,
36
- composedFrom: [BuiltInTemplateName.tsBase],
37
- composedOut: prebundleOutDirAbsolute,
38
- composedOnly: true,
39
- composedIncludeSegments: cliBundleOptions.includeSegments ?? bundleConfig.includeSegments,
40
- composedExcludeSegments: cliBundleOptions.excludeSegments ?? bundleConfig.excludeSegments,
41
- },
42
- });
43
- log.debug(`Bundling ${chalkHighlightThing(entry)} to ${chalkHighlightThing(outDirAbsolute)}`);
44
- await bundleConfig.build({
45
- outDir: outDirAbsolute,
46
- prebundleDir: prebundleOutDirAbsolute,
47
- entry,
48
- });
49
- log.debug(`Bundled index.ts to ${chalkHighlightThing(outDirAbsolute)}`);
50
- const requiresGroup = groupBy(Object.entries(bundleConfig.requires), ([, relativePath]) => relativePath);
51
- for (const [relativePath, group] of Object.entries(requiresGroup)) {
37
+ try {
52
38
  await generate({
53
39
  isEnsuringClient: false,
54
40
  isBundle: true,
@@ -59,18 +45,54 @@ export async function bundle({ projectInfo, fullSchema, cliBundleOptions, }) {
59
45
  cliGenerateOptions: {
60
46
  schemaPath: cliBundleOptions?.schemaPath,
61
47
  origin: cliBundleOptions?.origin,
62
- composedFrom: group.map(([templateName]) => templateName),
63
- composedOut: path.resolve(outDirAbsolute, relativePath),
48
+ openapiSpec: cliBundleOptions?.openapiSpec,
49
+ openapiGetModuleName: cliBundleOptions?.openapiGetModuleName,
50
+ openapiGetMethodName: cliBundleOptions?.openapiGetMethodName,
51
+ openapiRootUrl: cliBundleOptions?.openapiRootUrl,
52
+ openapiMixinName: cliBundleOptions?.openapiMixinName,
53
+ openapiFallback: cliBundleOptions?.openapiFallback,
54
+ composedFrom: [BuiltInTemplateName.tsBase],
55
+ composedOut: prebundleOutDirAbsolute,
64
56
  composedOnly: true,
57
+ composedIncludeSegments: includeSegments,
58
+ composedExcludeSegments: excludeSegments,
65
59
  },
66
60
  });
61
+ log.debug(`Bundling ${chalkHighlightThing(entry)} to ${chalkHighlightThing(outDirAbsolute)}`);
62
+ await bundleConfig.build({
63
+ outDir: outDirAbsolute,
64
+ prebundleDir: prebundleOutDirAbsolute,
65
+ entry,
66
+ });
67
+ log.debug(`Bundled index.ts to ${chalkHighlightThing(outDirAbsolute)}`);
68
+ const requiresGroup = groupBy(Object.entries(bundleConfig.requires), ([, relativePath]) => relativePath);
69
+ for (const [relativePath, group] of Object.entries(requiresGroup)) {
70
+ await generate({
71
+ isEnsuringClient: false,
72
+ isBundle: true,
73
+ projectInfo,
74
+ forceNothingWrittenLog: true,
75
+ fullSchema,
76
+ locatedSegments,
77
+ cliGenerateOptions: {
78
+ schemaPath: cliBundleOptions?.schemaPath,
79
+ origin: cliBundleOptions?.origin,
80
+ composedFrom: group.map(([templateName]) => templateName),
81
+ composedOut: path.resolve(outDirAbsolute, relativePath),
82
+ composedOnly: true,
83
+ },
84
+ });
85
+ }
67
86
  }
68
- if (!keepPrebundleDir) {
69
- await fs.rm(prebundleOutDirAbsolute, { recursive: true, force: true });
70
- log.debug(`Deleted temporary TypeScript client output directory: ${chalkHighlightThing(prebundleOutDirAbsolute)}`);
71
- }
72
- else {
73
- log.debug(`Temporary TypeScript client output directory not deleted because it is marked to keep: ${chalkHighlightThing(prebundleOutDirAbsolute)}`);
87
+ finally {
88
+ // clean up the prebundle dir even when generation or build fails
89
+ if (!keepPrebundleDir) {
90
+ await fs.rm(prebundleOutDirAbsolute, { recursive: true, force: true });
91
+ log.debug(`Deleted temporary TypeScript client output directory: ${chalkHighlightThing(prebundleOutDirAbsolute)}`);
92
+ }
93
+ else {
94
+ log.debug(`Temporary TypeScript client output directory not deleted because it is marked to keep: ${chalkHighlightThing(prebundleOutDirAbsolute)}`);
95
+ }
74
96
  }
75
97
  log.info(`Bundled TypeScript client to ${chalkHighlightThing(outDirAbsolute)}`);
76
98
  }
@@ -15,22 +15,5 @@ export interface DiffResult {
15
15
  }
16
16
  export declare function diffHandlers<T extends VovkControllerSchema['handlers']>(oldHandlers: T, newHandlers: T, nameOfClass: string): HandlersDiff;
17
17
  export declare function diffControllers<T extends VovkSegmentSchema['controllers']>(oldItems: T, newItems: T): ControllersDiff;
18
- /**
19
- example output:
20
- {
21
- controllers: {
22
- added: ["ControllerC"],
23
- removed: ["ControllerB"],
24
- handlers: [
25
- {
26
- nameOfClass: "ControllerA",
27
- added: ["handlerF"],
28
- removed: [],
29
- changed: ["handlerD"]
30
- }
31
- ]
32
- }
33
- }
34
- */
35
18
  export declare function diffSegmentSchema(oldJson: VovkSegmentSchema, newJson: VovkSegmentSchema): DiffResult;
36
19
  export {};
@@ -40,23 +40,7 @@ export function diffControllers(oldItems, newItems) {
40
40
  }
41
41
  return { added, removed, handlers: handlersDiff };
42
42
  }
43
- /**
44
- example output:
45
- {
46
- controllers: {
47
- added: ["ControllerC"],
48
- removed: ["ControllerB"],
49
- handlers: [
50
- {
51
- nameOfClass: "ControllerA",
52
- added: ["handlerF"],
53
- removed: [],
54
- changed: ["handlerD"]
55
- }
56
- ]
57
- }
58
- }
59
- */
43
+ // returns e.g. { controllers: { added: [...], removed: [...], handlers: [{ nameOfClass, added, removed, changed }] } }
60
44
  export function diffSegmentSchema(oldJson, newJson) {
61
45
  return {
62
46
  controllers: diffControllers(oldJson.controllers ?? {}, newJson.controllers ?? {}),
@@ -2,9 +2,9 @@ import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import debounce from 'lodash/debounce.js';
4
4
  import { VovkSchemaIdEnum } from 'vovk/internal';
5
- import { writeOneSegmentSchemaFile, META_FILE_NAME, ROOT_SEGMENT_FILE_NAME } from './write-one-segment-schema-file.mjs';
6
5
  import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
7
6
  import { writeMetaJson } from './write-meta-json.mjs';
7
+ import { META_FILE_NAME, ROOT_SEGMENT_FILE_NAME, writeOneSegmentSchemaFile } from './write-one-segment-schema-file.mjs';
8
8
  /**
9
9
  * Ensure that the schema files are created to avoid any import errors.
10
10
  */
@@ -31,20 +31,34 @@ export async function ensureSchemaFiles(projectInfo, schemaOutAbsolutePath, segm
31
31
  hasChanged = true;
32
32
  }
33
33
  }));
34
- // Recursive function to delete unnecessary JSON files and folders
34
+ // only a file carrying the vovk $schema id is ours to delete
35
+ async function isVovkSchemaFile(absolutePath) {
36
+ try {
37
+ const parsed = JSON.parse(await fs.readFile(absolutePath, 'utf-8'));
38
+ return typeof parsed.$schema === 'string' && parsed.$schema.startsWith('https://vovk.dev/api/schema/');
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ // Recursive function to delete unnecessary JSON files and folders, returns true if anything was deleted
35
45
  async function deleteUnnecessaryJsonFiles(dirPath, allow = [`${META_FILE_NAME}.json`]) {
36
46
  const entries = await fs.readdir(dirPath, { withFileTypes: true });
47
+ let hasDeleted = false;
37
48
  await Promise.all(entries.map(async (entry) => {
38
49
  const absolutePath = path.join(dirPath, entry.name);
39
50
  if (entry.isDirectory()) {
40
51
  // Recursively delete unnecessary files and folders within nested directories
41
- await deleteUnnecessaryJsonFiles(absolutePath);
42
- // Check if the directory is empty after deletion and remove it if so
43
- const remainingEntries = await fs.readdir(absolutePath);
44
- if (remainingEntries.length === 0) {
45
- await fs.rmdir(absolutePath);
46
- projectInfo?.log.debug(`Deleted unnecessary schema directory "${absolutePath}"`);
47
- hasChanged = true;
52
+ const deletedInside = await deleteUnnecessaryJsonFiles(absolutePath);
53
+ // remove the directory only when this cleanup emptied it, a pre-existing empty dir is not ours
54
+ if (deletedInside) {
55
+ const remainingEntries = await fs.readdir(absolutePath);
56
+ if (remainingEntries.length === 0) {
57
+ await fs.rmdir(absolutePath);
58
+ projectInfo?.log.debug(`Deleted unnecessary schema directory "${absolutePath}"`);
59
+ hasChanged = true;
60
+ hasDeleted = true;
61
+ }
48
62
  }
49
63
  }
50
64
  else if (entry.isFile() && entry.name.endsWith('.json')) {
@@ -52,13 +66,17 @@ export async function ensureSchemaFiles(projectInfo, schemaOutAbsolutePath, segm
52
66
  const segmentName = relativePath.replace(/\\/g, '/').slice(0, -5); // Remove '.json' extension
53
67
  if (!segmentNames.includes(segmentName) &&
54
68
  !segmentNames.includes(segmentName.replace(ROOT_SEGMENT_FILE_NAME, '')) &&
55
- !allow.includes(entry.name)) {
69
+ !allow.includes(entry.name) &&
70
+ // never delete a json file vovk did not write, schemaOutDir may point at user dirs
71
+ (await isVovkSchemaFile(absolutePath))) {
56
72
  await fs.unlink(absolutePath);
57
73
  projectInfo?.log.debug(`Deleted unnecessary schema file for ${formatLoggedSegmentName(segmentName)}`);
58
74
  hasChanged = true;
75
+ hasDeleted = true;
59
76
  }
60
77
  }
61
78
  }));
79
+ return hasDeleted;
62
80
  }
63
81
  await deleteUnnecessaryJsonFiles(schemaOutAbsolutePath);
64
82
  if (hasChanged)
@@ -1,25 +1,25 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { VovkSchemaIdEnum } from 'vovk/internal';
4
3
  import * as chokidar from 'chokidar';
5
- import { Agent, setGlobalDispatcher } from 'undici';
6
- import keyBy from 'lodash/keyBy.js';
7
4
  import capitalize from 'lodash/capitalize.js';
8
5
  import debounce from 'lodash/debounce.js';
9
- import once from 'lodash/once.js';
10
6
  import isEmpty from 'lodash/isEmpty.js';
11
- import { ensureSchemaFiles, debouncedEnsureSchemaFiles } from './ensure-schema-files.mjs';
12
- import { writeOneSegmentSchemaFile } from './write-one-segment-schema-file.mjs';
13
- import { logDiffResult } from './log-diff-result.mjs';
7
+ import keyBy from 'lodash/keyBy.js';
8
+ import once from 'lodash/once.js';
9
+ import { Agent, setGlobalDispatcher } from 'undici';
10
+ import { VovkSchemaIdEnum } from 'vovk/internal';
14
11
  import { ensureClient } from '../generate/ensure-client.mjs';
15
- import { getProjectInfo } from '../get-project-info/index.mjs';
16
12
  import { generate } from '../generate/generate.mjs';
17
- import { locateSegments } from '../utils/locate-segments.mjs';
13
+ import { getMetaSchema } from '../get-project-info/get-meta-schema.mjs';
14
+ import { getProjectInfo } from '../get-project-info/index.mjs';
15
+ import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
18
16
  import { debounceWithArgs } from '../utils/debounce-with-args.mjs';
19
17
  import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
18
+ import { locateSegments } from '../utils/locate-segments.mjs';
19
+ import { debouncedEnsureSchemaFiles, ensureSchemaFiles } from './ensure-schema-files.mjs';
20
+ import { logDiffResult } from './log-diff-result.mjs';
20
21
  import { writeMetaJson } from './write-meta-json.mjs';
21
- import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
22
- import { getMetaSchema } from '../get-project-info/get-meta-schema.mjs';
22
+ import { writeOneSegmentSchemaFile } from './write-one-segment-schema-file.mjs';
23
23
  export class VovkDev {
24
24
  #projectInfo;
25
25
  #segments = [];
@@ -33,7 +33,8 @@ export class VovkDev {
33
33
  #logLevel;
34
34
  constructor({ schemaOut, devHttps, logLevel }) {
35
35
  this.#schemaOut = schemaOut || null;
36
- this.#devHttps = devHttps || false;
36
+ // null when the flag is omitted so config.devHttps can take effect
37
+ this.#devHttps = devHttps ?? null;
37
38
  this.#logLevel = logLevel || 'info';
38
39
  }
39
40
  #watchSegments = (callback) => {
@@ -304,6 +305,11 @@ export class VovkDev {
304
305
  return;
305
306
  }
306
307
  log.debug(`Handling received schema from ${formatLoggedSegmentName(segmentName)}`);
308
+ // the write path is built from segmentName, an http response must not name a different segment
309
+ if ((segmentSchema.segmentName ?? '') !== segmentName) {
310
+ log.error(`Schema for ${formatLoggedSegmentName(segmentName)} reported a different segment name ${JSON.stringify(segmentSchema.segmentName)}, ignoring it`);
311
+ return;
312
+ }
307
313
  const schemaOutAbsolutePath = path.resolve(cwd, this.#schemaOut ?? config.schemaOutDir);
308
314
  const segment = this.#segments.find((s) => s.segmentName === segmentName);
309
315
  if (!segment) {
@@ -402,7 +408,7 @@ const env = process.env;
402
408
  if (env.__VOVK_START_WATCHER_IN_STANDALONE_MODE__ === 'true') {
403
409
  void new VovkDev({
404
410
  schemaOut: env.__VOVK_SCHEMA_OUT_FLAG__ || undefined,
405
- devHttps: env.__VOVK_DEV_HTTPS_FLAG__ === 'true',
411
+ devHttps: env.__VOVK_DEV_HTTPS_FLAG__ === 'true' || undefined,
406
412
  logLevel: env.__VOVK_LOG_LEVEL__,
407
413
  }).start({
408
414
  exit: env.__VOVK_EXIT__ === 'true',
@@ -1,3 +1,3 @@
1
- import type { DiffResult } from './diff-segment-schema.mjs';
2
1
  import type { ProjectInfo } from '../get-project-info/index.mjs';
2
+ import type { DiffResult } from './diff-segment-schema.mjs';
3
3
  export declare function logDiffResult(segmentName: string, diffResult: DiffResult, projectInfo: ProjectInfo): void;
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
- import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
3
2
  import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
3
+ import { formatLoggedSegmentName } from '../utils/format-logged-segment-name.mjs';
4
4
  export function logDiffResult(segmentName, diffResult, projectInfo) {
5
5
  const diffNormalized = [];
6
6
  diffResult.controllers.added.forEach((name) => {
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { META_FILE_NAME } from './write-one-segment-schema-file.mjs';
4
- import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
5
3
  import { getMetaSchema } from '../get-project-info/get-meta-schema.mjs';
4
+ import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
5
+ import { META_FILE_NAME } from './write-one-segment-schema-file.mjs';
6
6
  export async function writeMetaJson(schemaOutAbsolutePath, projectInfo) {
7
7
  const metaJsonPath = path.join(schemaOutAbsolutePath, `${META_FILE_NAME}.json`);
8
8
  const metaStr = JSON.stringify(getMetaSchema({
@@ -1,11 +1,16 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
3
- import { diffSegmentSchema } from './diff-segment-schema.mjs';
2
+ import path from 'node:path';
4
3
  import { getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
4
+ import { diffSegmentSchema } from './diff-segment-schema.mjs';
5
5
  export const ROOT_SEGMENT_FILE_NAME = 'root';
6
6
  export const META_FILE_NAME = '_meta';
7
7
  export async function writeOneSegmentSchemaFile({ schemaOutAbsolutePath, segmentSchema, skipIfExists = false, }) {
8
8
  const segmentPath = path.join(schemaOutAbsolutePath, `${segmentSchema.segmentName || ROOT_SEGMENT_FILE_NAME}.json`);
9
+ // segmentName may come from an http response, keep the write inside the schema out dir
10
+ const relativeToOut = path.relative(schemaOutAbsolutePath, segmentPath);
11
+ if (relativeToOut.startsWith(`..${path.sep}`) || relativeToOut === '..' || path.isAbsolute(relativeToOut)) {
12
+ throw new Error(`Refusing to write schema outside ${schemaOutAbsolutePath} for segment ${JSON.stringify(segmentSchema.segmentName)}`);
13
+ }
9
14
  if (skipIfExists && (await getFileSystemEntryType(segmentPath))) {
10
15
  try {
11
16
  await fs.stat(segmentPath);
@@ -1,6 +1,6 @@
1
1
  import { VovkSchemaIdEnum } from 'vovk/internal';
2
- import { generate } from './generate.mjs';
3
2
  import { getMetaSchema } from '../get-project-info/get-meta-schema.mjs';
3
+ import { generate } from './generate.mjs';
4
4
  const getEmptySegmentRecordSchema = (segmentNames) => {
5
5
  const result = {};
6
6
  for (const segmentName of segmentNames) {
@@ -1,40 +1,48 @@
1
- import path from 'node:path';
2
1
  import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { getTsconfig } from 'get-tsconfig';
3
4
  import matter from 'gray-matter';
4
5
  import _ from 'lodash';
5
6
  import { openAPIToVovkSchema, vovkSchemaToOpenAPI } from 'vovk/internal';
6
- import { getClientTemplateFiles } from './get-client-template-files.mjs';
7
+ import { ROOT_SEGMENT_FILE_NAME } from '../dev/write-one-segment-schema-file.mjs';
8
+ import { BuiltInTemplateName } from '../get-project-info/get-config/get-template-defs.mjs';
7
9
  import { chalkHighlightThing } from '../utils/chalk-highlight-thing.mjs';
10
+ import { normalizeOpenAPIMixin } from '../utils/normalize-openapi-mixin.mjs';
8
11
  import { pickSegmentFullSchema } from '../utils/pick-segment-full-schema.mjs';
9
12
  import { removeUnlistedDirectories } from '../utils/remove-unlisted-directories.mjs';
10
- import { writeOneClientFile, normalizeOutTemplatePath } from './write-one-client-file.mjs';
11
- import { ROOT_SEGMENT_FILE_NAME } from '../dev/write-one-segment-schema-file.mjs';
12
- import { getTsconfig } from 'get-tsconfig';
13
- import { normalizeOpenAPIMixin } from '../utils/normalize-openapi-mixin.mjs';
14
- import { BuiltInTemplateName } from '../get-project-info/get-config/get-template-defs.mjs';
13
+ import { getClientTemplateFiles } from './get-client-template-files.mjs';
14
+ import { normalizeOutTemplatePath, writeOneClientFile } from './write-one-client-file.mjs';
15
15
  const getIncludedSegmentNames = (config, fullSchema, configKey, cliGenerateOptions) => {
16
16
  const segments = Object.values(fullSchema.segments);
17
- const includeSegments = cliGenerateOptions?.[configKey === 'segmentedClient' ? 'segmentedIncludeSegments' : 'composedIncludeSegments'] ??
18
- config[configKey].includeSegments;
19
- const excludeSegments = cliGenerateOptions?.[configKey === 'segmentedClient' ? 'segmentedExcludeSegments' : 'composedExcludeSegments'] ??
20
- config[configKey].excludeSegments;
17
+ const cliIncludeSegments = cliGenerateOptions?.[configKey === 'segmentedClient' ? 'segmentedIncludeSegments' : 'composedIncludeSegments'];
18
+ const cliExcludeSegments = cliGenerateOptions?.[configKey === 'segmentedClient' ? 'segmentedExcludeSegments' : 'composedExcludeSegments'];
19
+ // CLI options win as a pair so config exclude cannot conflict with CLI include
20
+ const isFromCli = !!(cliIncludeSegments?.length || cliExcludeSegments?.length);
21
+ const includeSegments = isFromCli ? cliIncludeSegments : config[configKey].includeSegments;
22
+ const excludeSegments = isFromCli ? cliExcludeSegments : config[configKey].excludeSegments;
21
23
  if (includeSegments?.length && excludeSegments?.length) {
22
- throw new Error(`Both includeSegments and excludeSegments are set in "${configKey}" config. Please use only one of them.`);
24
+ throw new Error(`Both includeSegments and excludeSegments are set ${isFromCli ? 'as CLI options' : `in "${configKey}" config`}. Please use only one of them.`);
23
25
  }
24
- const includedSegmentNames = Array.isArray(includeSegments) && includeSegments.length
25
- ? includeSegments.map((segmentName) => {
26
- const segment = segments.find(({ segmentName: sName }) => sName === segmentName);
27
- if (!segment) {
26
+ const segmentExists = (segmentName) => segments.some(({ segmentName: sName }) => sName === segmentName);
27
+ if (includeSegments?.length) {
28
+ for (const segmentName of includeSegments) {
29
+ if (!segmentExists(segmentName)) {
28
30
  throw new Error(`Segment "${segmentName}" not found in the config for "${configKey}"`);
29
31
  }
30
- return segment.segmentName;
31
- })
32
- : Array.isArray(excludeSegments) && excludeSegments.length // TODO: Warn if excludeSegments includes a segment name that is not listed at segments
33
- ? segments
34
- .filter(({ segmentName }) => !excludeSegments?.includes(segmentName))
35
- .map(({ segmentName }) => segmentName)
36
- : segments.map(({ segmentName }) => segmentName);
37
- return includedSegmentNames;
32
+ }
33
+ return includeSegments;
34
+ }
35
+ if (excludeSegments?.length) {
36
+ for (const segmentName of excludeSegments) {
37
+ if (!segmentExists(segmentName)) {
38
+ throw new Error(`Segment "${segmentName}" from excludeSegments not found in the config for "${configKey}"`);
39
+ }
40
+ }
41
+ return segments
42
+ .filter(({ segmentName }) => !excludeSegments.includes(segmentName))
43
+ .map(({ segmentName }) => segmentName);
44
+ }
45
+ return segments.map(({ segmentName }) => segmentName);
38
46
  };
39
47
  function logClientGenerationResults({ results, log, isEnsuringClient = false, forceNothingWrittenLog = false, clientType = 'Composed', startTime, fromTemplates, }) {
40
48
  const writtenResults = results.filter(({ written }) => written);
@@ -221,6 +229,8 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
221
229
  cliGenerateOptions,
222
230
  configKey: 'segmentedClient',
223
231
  });
232
+ // what a generated file may look like inside a segment directory, used to spare user files when pruning
233
+ const generatedRelPaths = segmentedClientTemplateFiles.map(({ templateFilePath, relativeDir }) => path.join(relativeDir, path.basename(templateFilePath).replace(/\.ejs$/, '')));
224
234
  const segmentedClientResults = await Promise.all(segmentedClientTemplateFiles.map(async (clientTemplateFile) => {
225
235
  const { templateFilePath, templateName, templateDef, outCwdRelativeDir } = clientTemplateFile;
226
236
  const templateContent = await fs.readFile(templateFilePath, 'utf-8');
@@ -280,7 +290,10 @@ export async function generate({ isEnsuringClient = false, isBundle = false, pro
280
290
  }));
281
291
  const outAbsoluteDir = path.resolve(cwd, outCwdRelativeDir);
282
292
  // Remove unlisted directories in the output directory
283
- await removeUnlistedDirectories(outAbsoluteDir, segmentNames.map((s) => s || ROOT_SEGMENT_FILE_NAME));
293
+ const skippedDirs = await removeUnlistedDirectories(outAbsoluteDir, segmentNames.map((s) => s || ROOT_SEGMENT_FILE_NAME), generatedRelPaths);
294
+ for (const skippedDir of skippedDirs) {
295
+ log.warn(`Directory ${chalkHighlightThing(skippedDir)} is not a known segment but holds files that were not generated, so it is left untouched.`);
296
+ }
284
297
  return {
285
298
  written: results.filter((result) => !!result).some(({ written }) => written),
286
299
  templateName,
@@ -1,10 +1,10 @@
1
1
  import path from 'node:path';
2
2
  import { glob } from 'glob';
3
- import { resolveAbsoluteModulePath } from '../utils/resolve-absolute-module-path.mjs';
4
- import { getFileSystemEntryType, FileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
5
- import { getPublicModuleNameFromPath } from '../utils/get-public-module-name-from-path.mjs';
6
- import omit from 'lodash/omit.js';
7
3
  import merge from 'lodash/merge.js';
4
+ import omit from 'lodash/omit.js';
5
+ import { FileSystemEntryType, getFileSystemEntryType } from '../utils/get-file-system-entry-type.mjs';
6
+ import { getPublicModuleNameFromPath } from '../utils/get-public-module-name-from-path.mjs';
7
+ import { resolveAbsoluteModulePath } from '../utils/resolve-absolute-module-path.mjs';
8
8
  export async function getClientTemplateFiles({ config, cwd, log, configKey, cliGenerateOptions, }) {
9
9
  const usedTemplateDefs = {};
10
10
  const fromTemplates = configKey === 'composedClient'
@@ -18,6 +18,10 @@ export async function getClientTemplateFiles({ config, cwd, log, configKey, cliG
18
18
  const configOutDir = config[configKey].outDir;
19
19
  for (const templateName of fromTemplates) {
20
20
  if (!(templateName in config.clientTemplateDefs)) {
21
+ // js family removed in v4
22
+ if (['js', 'jsBase', 'schemaJs', 'openapiJs'].includes(templateName)) {
23
+ throw new Error(`The "${templateName}" template was removed in v4. Use "ts" instead.`);
24
+ }
21
25
  throw new Error(`Unknown template name: ${templateName}`);
22
26
  }
23
27
  usedTemplateDefs[templateName] = config.clientTemplateDefs[templateName];
@@ -1,6 +1,6 @@
1
1
  import type { VovkSchema } from 'vovk';
2
- import type { ProjectInfo } from '../get-project-info/index.mjs';
3
2
  import { type VovkStrictConfig } from 'vovk/internal';
3
+ import type { ProjectInfo } from '../get-project-info/index.mjs';
4
4
  export declare function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, config, }: {
5
5
  schemaOutAbsolutePath: string;
6
6
  isNextInstalled: boolean;
@@ -1,9 +1,9 @@
1
- import { readFile, access } from 'node:fs/promises';
1
+ import { access, readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { glob } from 'glob';
4
+ import { deepExtend, VovkSchemaIdEnum } from 'vovk/internal';
4
5
  import { META_FILE_NAME, ROOT_SEGMENT_FILE_NAME } from '../dev/write-one-segment-schema-file.mjs';
5
6
  import { getMetaSchema } from '../get-project-info/get-meta-schema.mjs';
6
- import { deepExtend, VovkSchemaIdEnum } from 'vovk/internal';
7
7
  export async function getProjectFullSchema({ schemaOutAbsolutePath, isNextInstalled, log, config, }) {
8
8
  const result = {
9
9
  $schema: VovkSchemaIdEnum.SCHEMA,
@@ -1,6 +1,6 @@
1
1
  import type { VovkSchema } from 'vovk';
2
- import type { GenerateOptions } from '../types.mjs';
3
2
  import type { ProjectInfo } from '../get-project-info/index.mjs';
3
+ import type { GenerateOptions } from '../types.mjs';
4
4
  export declare class VovkGenerate {
5
5
  #private;
6
6
  constructor({ cliGenerateOptions, projectInfo, forceNothingWrittenLog, }: {
@@ -8,7 +8,7 @@ export declare class VovkGenerate {
8
8
  projectInfo: ProjectInfo;
9
9
  forceNothingWrittenLog?: boolean;
10
10
  });
11
- start(): void;
11
+ start(): Promise<void> | undefined;
12
12
  generate(): Promise<void>;
13
13
  getFullSchema(): Promise<VovkSchema>;
14
14
  watch({ throttleDelay }: {