@rexeus/typeweaver 0.0.3 → 0.0.4

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,12 @@
1
1
  import * as path from 'path';
2
2
  import path__default, { dirname } from 'path';
3
+ import url, { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url';
4
+ import { Command } from 'commander';
3
5
  import fs, { realpathSync, statSync } from 'fs';
4
- import { HttpResponseDefinition, HttpOperationDefinition, HttpStatusCodeNameMap } from '@rexeus/typeweaver-core';
6
+ import { PluginRegistry, PluginContextBuilder } from '@rexeus/typeweaver-gen';
7
+ import TypesPlugin from '@rexeus/typeweaver-types';
8
+ import { render } from 'ejs';
5
9
  import { createRequire, builtinModules } from 'module';
6
- import url, { fileURLToPath, pathToFileURL, URL as URL$1 } from 'url';
7
10
  import process3 from 'process';
8
11
  import os from 'os';
9
12
  import tty from 'tty';
@@ -11,203 +14,140 @@ import fs$1 from 'fs/promises';
11
14
  import assert2 from 'assert';
12
15
  import v8 from 'v8';
13
16
  import { format, inspect } from 'util';
14
- import { PluginRegistry, PluginContextBuilder } from '@rexeus/typeweaver-gen';
15
- import ejs from 'ejs';
16
- import TypesPlugin from '@rexeus/typeweaver-types';
17
- import { Command } from 'commander';
17
+ import { HttpMethod, HttpStatusCode, HttpResponseDefinition, HttpOperationDefinition, HttpStatusCodeNameMap } from '@rexeus/typeweaver-core';
18
+ import { z } from 'zod/v4';
18
19
 
19
- class InvalidSharedDirError extends Error {
20
- constructor(explanation) {
21
- super("Invalid shared dir");
22
- this.explanation = explanation;
20
+ var version = "0.0.4";
21
+ var packageJson = {
22
+ version: version};
23
+
24
+ class IndexFileGenerator {
25
+ constructor(templateDir) {
26
+ this.templateDir = templateDir;
27
+ }
28
+ generate(context) {
29
+ const templateFilePath = path__default.join(this.templateDir, "Index.ejs");
30
+ const template = fs.readFileSync(templateFilePath, "utf8");
31
+ const indexPaths = /* @__PURE__ */ new Set();
32
+ for (const generatedFile of context.getGeneratedFiles()) {
33
+ indexPaths.add(`./${generatedFile.replace(/\.ts$/, "")}`);
34
+ }
35
+ const content = render(template, {
36
+ indexPaths: Array.from(indexPaths).sort()
37
+ });
38
+ fs.writeFileSync(path__default.join(context.outputDir, "index.ts"), content);
23
39
  }
24
40
  }
25
41
 
26
- class InvalidSharedResponseDefinitionError extends Error {
27
- constructor(fileName, dir, file, explanation) {
28
- super("Invalid shared response definition");
29
- this.fileName = fileName;
30
- this.dir = dir;
31
- this.file = file;
32
- this.explanation = explanation;
42
+ class PluginLoadingFailure extends Error {
43
+ constructor(pluginName, attempts) {
44
+ super(`Failed to load plugin '${pluginName}'`);
45
+ this.pluginName = pluginName;
46
+ this.attempts = attempts;
47
+ Object.setPrototypeOf(this, PluginLoadingFailure.prototype);
33
48
  }
34
49
  }
35
50
 
36
- class ResourceReader {
37
- constructor(config) {
38
- this.config = config;
51
+ class PluginLoader {
52
+ constructor(registry, requiredPlugins, strategies = ["npm", "local"]) {
53
+ this.registry = registry;
54
+ this.requiredPlugins = requiredPlugins;
55
+ this.strategies = strategies;
39
56
  }
40
- async getResources() {
41
- const contents = fs.readdirSync(this.config.sourceDir, {
42
- withFileTypes: true
43
- });
44
- const result = {
45
- entityResources: {},
46
- sharedResponseResources: []
47
- };
48
- const sharedDefinitions = contents.find(
49
- (content) => content.name === "shared"
50
- );
51
- if (sharedDefinitions) {
52
- if (!sharedDefinitions.isDirectory()) {
53
- throw new InvalidSharedDirError("'shared' is a file, not a directory");
54
- }
55
- result.sharedResponseResources = await this.getSharedResponseResources();
56
- console.info(
57
- `Found '${result.sharedResponseResources.length}' shared responses`
58
- );
59
- } else {
60
- console.info("No 'shared' directory found");
57
+ /**
58
+ * Load all plugins from configuration
59
+ */
60
+ async loadPlugins(config) {
61
+ for (const requiredPlugin of this.requiredPlugins) {
62
+ this.registry.register(requiredPlugin);
61
63
  }
62
- for (const content of contents) {
63
- if (!content.isDirectory()) {
64
- console.info(`Skipping '${content.name}' as it is not a directory`);
65
- continue;
66
- }
67
- if (content.name === "shared") {
68
- continue;
69
- }
70
- const entityName = content.name;
71
- const entitySourceDir = path__default.join(this.config.sourceDir, entityName);
72
- const operationResources = await this.getEntityOperationResources(
73
- entitySourceDir,
74
- entityName
75
- );
76
- result.entityResources[entityName] = operationResources;
77
- console.info(
78
- `Found '${operationResources.length}' operation definitions for entity '${entityName}'`
79
- );
64
+ if (!config?.plugins) {
65
+ return;
80
66
  }
81
- return result;
82
- }
83
- async getSharedResponseResources() {
84
- const sharedContents = fs.readdirSync(this.config.sharedSourceDir, {
85
- withFileTypes: true
86
- });
87
- const sharedResponseResources = [];
88
- for (const content of sharedContents) {
89
- if (!content.isFile()) {
90
- console.info(`Skipping '${content.name}' as it is not a file`);
91
- continue;
92
- }
93
- const sourceFileName = content.name;
94
- const sourceFile = path__default.join(this.config.sharedSourceDir, sourceFileName);
95
- const definition = await import(sourceFile);
96
- if (!definition.default) {
97
- console.info(
98
- `Skipping '${sourceFile}' as it does not have a default export`
99
- );
100
- continue;
101
- }
102
- if (!(definition.default instanceof HttpResponseDefinition)) {
103
- console.info(
104
- `Skipping '${sourceFile}' as it is not an instance of HttpResponseDefinition`
105
- );
106
- continue;
67
+ const successful = [];
68
+ for (const plugin of config.plugins) {
69
+ let result;
70
+ if (typeof plugin === "string") {
71
+ result = await this.loadPlugin(plugin);
72
+ } else {
73
+ result = await this.loadPlugin(plugin[0]);
107
74
  }
108
- if (!definition.default.isShared) {
109
- throw new InvalidSharedResponseDefinitionError(
110
- sourceFileName,
111
- this.config.sharedSourceDir,
112
- sourceFile,
113
- "'isShared' property is not set to 'true'"
75
+ if (result.success === false) {
76
+ throw new PluginLoadingFailure(
77
+ result.error.pluginName,
78
+ result.error.attempts
114
79
  );
115
80
  }
116
- const outputDir = this.config.sharedOutputDir;
117
- const outputFileName = `${definition.default.name}Response.ts`;
118
- const outputFile = path__default.join(outputDir, outputFileName);
119
- sharedResponseResources.push({
120
- ...definition.default,
121
- isShared: true,
122
- sourceDir: this.config.sharedSourceDir,
123
- sourceFile,
124
- sourceFileName,
125
- outputFile,
126
- outputFileName,
127
- outputDir
128
- });
81
+ successful.push(result.value);
82
+ this.registry.register(result.value.plugin);
129
83
  }
130
- return sharedResponseResources;
84
+ this.reportSuccessfulLoads(successful);
131
85
  }
132
- async getEntityOperationResources(sourceDir, entityName) {
133
- const contents = fs.readdirSync(sourceDir, {
134
- withFileTypes: true
135
- });
136
- const definitions = [];
137
- for (const content of contents) {
138
- if (!content.isFile()) {
139
- console.info(`Skipping '${content.name}' as it is not a file`);
140
- continue;
141
- }
142
- const sourceFileName = content.name;
143
- const sourceFile = path__default.join(sourceDir, sourceFileName);
144
- const definition = await import(sourceFile);
145
- if (!definition.default) {
146
- console.info(
147
- `Skipping '${sourceFile}' as it does not have a default export`
148
- );
149
- continue;
86
+ /**
87
+ * Load a plugin from a string identifier
88
+ */
89
+ async loadPlugin(pluginName) {
90
+ const possiblePaths = this.generatePluginPaths(pluginName);
91
+ const attempts = [];
92
+ for (const possiblePath of possiblePaths) {
93
+ try {
94
+ const pluginPackage = await import(possiblePath);
95
+ if (pluginPackage.default) {
96
+ return {
97
+ success: true,
98
+ value: {
99
+ plugin: new pluginPackage.default(),
100
+ source: possiblePath
101
+ }
102
+ };
103
+ }
104
+ attempts.push({
105
+ path: possiblePath,
106
+ error: "No default export found"
107
+ });
108
+ } catch (error) {
109
+ attempts.push({
110
+ path: possiblePath,
111
+ error: error instanceof Error ? error.message : String(error)
112
+ });
150
113
  }
151
- if (!(definition.default instanceof HttpOperationDefinition)) {
152
- console.info(
153
- `Skipping '${sourceFile}' as it is not an instance of HttpOperationDefinition`
154
- );
155
- continue;
114
+ }
115
+ return {
116
+ success: false,
117
+ error: {
118
+ pluginName,
119
+ attempts
156
120
  }
157
- const { operationId } = definition.default;
158
- const outputDir = path__default.join(this.config.outputDir, entityName);
159
- const outputRequestFileName = `${operationId}Request.ts`;
160
- const outputRequestFile = path__default.join(outputDir, outputRequestFileName);
161
- const outputResponseFileName = `${operationId}Response.ts`;
162
- const outputResponseFile = path__default.join(outputDir, outputResponseFileName);
163
- const outputRequestValidationFileName = `${operationId}RequestValidator.ts`;
164
- const outputRequestValidationFile = path__default.join(
165
- outputDir,
166
- outputRequestValidationFileName
167
- );
168
- const outputResponseValidationFileName = `${operationId}ResponseValidator.ts`;
169
- const outputResponseValidationFile = path__default.join(
170
- outputDir,
171
- outputResponseValidationFileName
172
- );
173
- const outputClientFileName = `${operationId}Client.ts`;
174
- const outputClientFile = path__default.join(outputDir, outputClientFileName);
175
- const operationResource = {
176
- sourceDir,
177
- sourceFile,
178
- sourceFileName,
179
- definition: {
180
- ...definition.default,
181
- responses: []
182
- },
183
- outputDir,
184
- entityName,
185
- outputRequestFile,
186
- outputResponseFile,
187
- outputResponseValidationFile,
188
- outputRequestValidationFile,
189
- outputRequestFileName,
190
- outputRequestValidationFileName,
191
- outputResponseFileName,
192
- outputResponseValidationFileName,
193
- outputClientFile,
194
- outputClientFileName
195
- };
196
- if (!definition.default.responses) {
197
- throw new Error(
198
- `Operation '${operationId}' does not have any responses`
199
- );
121
+ };
122
+ }
123
+ /**
124
+ * Generate possible plugin paths based on configured strategies
125
+ */
126
+ generatePluginPaths(pluginName) {
127
+ const paths = [];
128
+ for (const strategy of this.strategies) {
129
+ switch (strategy) {
130
+ case "npm":
131
+ paths.push(`@rexeus/typeweaver-${pluginName}`);
132
+ paths.push(`@rexeus/${pluginName}`);
133
+ break;
134
+ case "local":
135
+ paths.push(pluginName);
136
+ break;
200
137
  }
201
- for (const response of definition.default.responses) {
202
- const extendedResponse = {
203
- ...response,
204
- statusCodeName: HttpStatusCodeNameMap[response.statusCode]
205
- };
206
- operationResource.definition.responses.push(extendedResponse);
138
+ }
139
+ return paths;
140
+ }
141
+ /**
142
+ * Report successful plugin loads
143
+ */
144
+ reportSuccessfulLoads(successful) {
145
+ if (successful.length > 0) {
146
+ console.info(`Successfully loaded ${successful.length} plugin(s):`);
147
+ for (const result of successful) {
148
+ console.info(` - ${result.plugin.name} (from ${result.source})`);
207
149
  }
208
- definitions.push(operationResource);
209
150
  }
210
- return definitions;
211
151
  }
212
152
  }
213
153
 
@@ -22453,11 +22393,11 @@ var { parsers, printers } = createParsersAndPrinters([
22453
22393
  printers: ["estree", "estree-json"]
22454
22394
  },
22455
22395
  {
22456
- importPlugin: () => import('./flow--vV0j3Y-.js'),
22396
+ importPlugin: () => import('./flow-q2wMXrDa.js'),
22457
22397
  parsers: ["flow"]
22458
22398
  },
22459
22399
  {
22460
- importPlugin: () => import('./glimmer-B-ODUU1A.js'),
22400
+ importPlugin: () => import('./glimmer-wgjvri6H.js'),
22461
22401
  parsers: ["glimmer"],
22462
22402
  printers: ["glimmer"]
22463
22403
  },
@@ -22472,7 +22412,7 @@ var { parsers, printers } = createParsersAndPrinters([
22472
22412
  printers: ["html"]
22473
22413
  },
22474
22414
  {
22475
- importPlugin: () => import('./markdown-Xi16tYTk.js'),
22415
+ importPlugin: () => import('./markdown-Nz6Lc3gB.js'),
22476
22416
  parsers: ["markdown", "mdx", "remark"],
22477
22417
  printers: ["mdast"]
22478
22418
  },
@@ -22481,16 +22421,16 @@ var { parsers, printers } = createParsersAndPrinters([
22481
22421
  parsers: ["meriyah"]
22482
22422
  },
22483
22423
  {
22484
- importPlugin: () => import('./postcss-DdgOJBTx.js'),
22424
+ importPlugin: () => import('./postcss-yEOijaXJ.js'),
22485
22425
  parsers: ["css", "less", "scss"],
22486
22426
  printers: ["postcss"]
22487
22427
  },
22488
22428
  {
22489
- importPlugin: () => import('./typescript-C4gnKzhB.js'),
22429
+ importPlugin: () => import('./typescript-Cv79a1Qz.js'),
22490
22430
  parsers: ["typescript"]
22491
22431
  },
22492
22432
  {
22493
- importPlugin: () => import('./yaml-B0tq6Ttj.js'),
22433
+ importPlugin: () => import('./yaml-DT3qlFoE.js'),
22494
22434
  parsers: ["yaml"],
22495
22435
  printers: ["yaml"]
22496
22436
  }
@@ -22810,7 +22750,6 @@ var debugApis = {
22810
22750
  printDocToString: withPlugins(printDocToString2),
22811
22751
  mockable: mockable_default
22812
22752
  };
22813
- var src_default = index_exports;
22814
22753
 
22815
22754
  class Prettier {
22816
22755
  constructor(outputDir) {
@@ -22825,7 +22764,7 @@ class Prettier {
22825
22764
  if (content.isFile()) {
22826
22765
  const filePath = path__default.join(targetDir, content.name);
22827
22766
  const unformatted = fs.readFileSync(filePath, "utf8");
22828
- const formatted = await src_default.format(unformatted, {
22767
+ const formatted = await format2(unformatted, {
22829
22768
  parser: "typescript"
22830
22769
  });
22831
22770
  fs.writeFileSync(filePath, formatted);
@@ -22836,133 +22775,823 @@ class Prettier {
22836
22775
  }
22837
22776
  }
22838
22777
 
22839
- class IndexFileGenerator {
22840
- constructor(templateDir) {
22841
- this.templateDir = templateDir;
22778
+ class DuplicateOperationIdError extends Error {
22779
+ constructor(operationId, firstFile, duplicateFile) {
22780
+ super(
22781
+ `Duplicate operation ID '${operationId}' found.
22782
+ First defined in: \`${firstFile}\`
22783
+ Duplicate found in: \`${duplicateFile}\``
22784
+ );
22785
+ this.operationId = operationId;
22786
+ this.firstFile = firstFile;
22787
+ this.duplicateFile = duplicateFile;
22842
22788
  }
22843
- generate(context) {
22844
- const templateFilePath = path__default.join(this.templateDir, "Index.ejs");
22845
- const template = fs.readFileSync(templateFilePath, "utf8");
22846
- const indexPaths = /* @__PURE__ */ new Set();
22847
- for (const generatedFile of context.getGeneratedFiles()) {
22848
- indexPaths.add(`./${generatedFile.replace(/\.ts$/, "")}`);
22789
+ }
22790
+
22791
+ class DuplicateResponseNameError extends Error {
22792
+ constructor(responseName, firstFile, duplicateFile) {
22793
+ super(
22794
+ `Duplicate response name '${responseName}' found.
22795
+ First defined in: \`${firstFile}\`
22796
+ Duplicate found in: \`${duplicateFile}\``
22797
+ );
22798
+ this.responseName = responseName;
22799
+ this.firstFile = firstFile;
22800
+ this.duplicateFile = duplicateFile;
22801
+ }
22802
+ }
22803
+
22804
+ class DuplicateRouteError extends Error {
22805
+ constructor(path, method, firstOperationId, firstFile, duplicateOperationId, duplicateFile) {
22806
+ super(
22807
+ `Duplicate route '${method} ${path}' found.
22808
+ First defined by operation '${firstOperationId}' in: \`${firstFile}\`
22809
+ Duplicate defined by operation '${duplicateOperationId}' in: \`${duplicateFile}\``
22810
+ );
22811
+ this.path = path;
22812
+ this.method = method;
22813
+ this.firstOperationId = firstOperationId;
22814
+ this.firstFile = firstFile;
22815
+ this.duplicateOperationId = duplicateOperationId;
22816
+ this.duplicateFile = duplicateFile;
22817
+ }
22818
+ }
22819
+
22820
+ class DefinitionRegistry {
22821
+ operationIds = /* @__PURE__ */ new Map();
22822
+ responseNames = /* @__PURE__ */ new Map();
22823
+ routes = /* @__PURE__ */ new Map();
22824
+ registerOperation(operation, sourceFile) {
22825
+ const existingFile = this.operationIds.get(operation.operationId);
22826
+ if (existingFile) {
22827
+ throw new DuplicateOperationIdError(
22828
+ operation.operationId,
22829
+ existingFile,
22830
+ sourceFile
22831
+ );
22849
22832
  }
22850
- const content = ejs.render(template, {
22851
- indexPaths: Array.from(indexPaths).sort()
22833
+ this.operationIds.set(operation.operationId, sourceFile);
22834
+ const normalizedPath = this.normalizePath(operation.path);
22835
+ const routeKey = `${operation.method} ${normalizedPath}`;
22836
+ const existingRoute = this.routes.get(routeKey);
22837
+ if (existingRoute) {
22838
+ throw new DuplicateRouteError(
22839
+ operation.path,
22840
+ operation.method,
22841
+ existingRoute.operationId,
22842
+ existingRoute.file,
22843
+ operation.operationId,
22844
+ sourceFile
22845
+ );
22846
+ }
22847
+ this.routes.set(routeKey, {
22848
+ operationId: operation.operationId,
22849
+ file: sourceFile
22850
+ });
22851
+ }
22852
+ registerResponse(response, sourceFile) {
22853
+ const existingFile = this.responseNames.get(response.name);
22854
+ if (existingFile) {
22855
+ throw new DuplicateResponseNameError(
22856
+ response.name,
22857
+ existingFile,
22858
+ sourceFile
22859
+ );
22860
+ }
22861
+ this.responseNames.set(response.name, sourceFile);
22862
+ }
22863
+ hasOperationId(operationId) {
22864
+ return this.operationIds.has(operationId);
22865
+ }
22866
+ hasResponseName(name) {
22867
+ return this.responseNames.has(name);
22868
+ }
22869
+ hasRoute(method, path) {
22870
+ const normalizedPath = this.normalizePath(path);
22871
+ const routeKey = `${method} ${normalizedPath}`;
22872
+ return this.routes.has(routeKey);
22873
+ }
22874
+ getOperationFile(operationId) {
22875
+ return this.operationIds.get(operationId);
22876
+ }
22877
+ getResponseFile(name) {
22878
+ return this.responseNames.get(name);
22879
+ }
22880
+ getRouteInfo(method, path) {
22881
+ const normalizedPath = this.normalizePath(path);
22882
+ const routeKey = `${method} ${normalizedPath}`;
22883
+ return this.routes.get(routeKey);
22884
+ }
22885
+ normalizePath(path) {
22886
+ let paramIndex = 1;
22887
+ return path.replace(/:([a-zA-Z0-9_]+)/g, () => {
22888
+ return `:param${paramIndex++}`;
22852
22889
  });
22853
- fs.writeFileSync(path__default.join(context.outputDir, "index.ts"), content);
22854
22890
  }
22855
22891
  }
22856
22892
 
22857
- class PluginLoadingFailure extends Error {
22858
- constructor(pluginName, attempts) {
22859
- super(`Failed to load plugin '${pluginName}'`);
22860
- this.pluginName = pluginName;
22861
- this.attempts = attempts;
22862
- Object.setPrototypeOf(this, PluginLoadingFailure.prototype);
22893
+ class EmptyResponseArrayError extends Error {
22894
+ constructor(operationId, file) {
22895
+ super(
22896
+ `Operation '${operationId}' has no responses defined at \`${file}\`
22897
+ Operations must have at least one response definition`
22898
+ );
22899
+ this.operationId = operationId;
22900
+ this.file = file;
22863
22901
  }
22864
22902
  }
22865
22903
 
22866
- class PluginLoader {
22867
- constructor(registry, requiredPlugins, strategies = ["npm", "local"]) {
22868
- this.registry = registry;
22869
- this.requiredPlugins = requiredPlugins;
22870
- this.strategies = strategies;
22904
+ class InvalidHttpMethodError extends Error {
22905
+ constructor(operationId, method, file) {
22906
+ const validMethods = Object.values(HttpMethod).join(", ");
22907
+ super(
22908
+ `Invalid HTTP method '${method}' in operation '${operationId}' at \`${file}\`
22909
+ Valid methods: ${validMethods}`
22910
+ );
22911
+ this.operationId = operationId;
22912
+ this.method = method;
22913
+ this.file = file;
22871
22914
  }
22872
- /**
22873
- * Load all plugins from configuration
22874
- */
22875
- async loadPlugins(config) {
22876
- for (const requiredPlugin of this.requiredPlugins) {
22877
- this.registry.register(requiredPlugin);
22915
+ }
22916
+
22917
+ class InvalidPathParameterError extends Error {
22918
+ constructor(operationId, path, issue, file) {
22919
+ super(
22920
+ `Invalid path parameters in operation '${operationId}' at \`${file}\`
22921
+ Path: ${path}
22922
+ Issue: ${issue}`
22923
+ );
22924
+ this.operationId = operationId;
22925
+ this.path = path;
22926
+ this.issue = issue;
22927
+ this.file = file;
22928
+ }
22929
+ }
22930
+
22931
+ class InvalidSchemaError extends Error {
22932
+ constructor(schemaType, definitionName, context, file) {
22933
+ const schemaRequirement = schemaType === "body" ? "Must be a Zod schema" : "Must be a Zod object schema";
22934
+ super(
22935
+ `Invalid ${schemaType} schema in ${context} '${definitionName}' at \`${file}\`. ${schemaRequirement}.`
22936
+ );
22937
+ this.schemaType = schemaType;
22938
+ this.definitionName = definitionName;
22939
+ this.context = context;
22940
+ this.file = file;
22941
+ }
22942
+ }
22943
+
22944
+ class InvalidSchemaShapeError extends Error {
22945
+ constructor(schemaType, definitionName, context, propertyName, invalidType, file) {
22946
+ const allowedTypes = schemaType === "param" ? "string-based types (ZodString, ZodLiteral<string>, ZodEnum) or ZodOptional of these types" : "string-based types (ZodString, ZodLiteral<string>, ZodEnum), ZodOptional, or ZodArray of these types";
22947
+ super(
22948
+ `Invalid ${schemaType} schema shape in ${context} '${definitionName}' at \`${file}\`
22949
+ Property '${propertyName}' has invalid type: ${invalidType}
22950
+ Allowed types: ${allowedTypes}`
22951
+ );
22952
+ this.schemaType = schemaType;
22953
+ this.definitionName = definitionName;
22954
+ this.context = context;
22955
+ this.propertyName = propertyName;
22956
+ this.invalidType = invalidType;
22957
+ this.file = file;
22958
+ }
22959
+ }
22960
+
22961
+ class InvalidStatusCodeError extends Error {
22962
+ constructor(statusCode, responseName, file) {
22963
+ const validStatusCodes = Object.values(HttpStatusCode).join(", ");
22964
+ super(
22965
+ `Invalid status code '${statusCode}' in response '${responseName}' at \`${file}\`
22966
+ Valid status codes: ${validStatusCodes}`
22967
+ );
22968
+ this.statusCode = statusCode;
22969
+ this.responseName = responseName;
22970
+ this.file = file;
22971
+ }
22972
+ }
22973
+
22974
+ class MissingRequiredFieldError extends Error {
22975
+ constructor(entityType, entityName, missingField, file) {
22976
+ super(
22977
+ `Missing required field '${missingField}' in ${entityType} '${entityName}' at \`${file}\``
22978
+ );
22979
+ this.entityType = entityType;
22980
+ this.entityName = entityName;
22981
+ this.missingField = missingField;
22982
+ this.file = file;
22983
+ }
22984
+ }
22985
+
22986
+ class DefinitionValidator {
22987
+ registry;
22988
+ constructor(registry) {
22989
+ this.registry = registry ?? new DefinitionRegistry();
22990
+ }
22991
+ validateOperation(operation, sourceFile) {
22992
+ this.registry.registerOperation(operation, sourceFile);
22993
+ this.validateOperationRequiredFields(operation, sourceFile);
22994
+ this.validateHttpMethod(operation, sourceFile);
22995
+ if (operation.request) {
22996
+ this.validateRequestSchemas(operation, sourceFile);
22997
+ }
22998
+ this.validateOperationResponses(operation, sourceFile);
22999
+ this.validatePathParameters(operation, sourceFile);
23000
+ }
23001
+ validateResponse(response, sourceFile) {
23002
+ this.registry.registerResponse(response, sourceFile);
23003
+ this.validateResponseRequiredFields(response, sourceFile);
23004
+ this.validateStatusCode(response, sourceFile);
23005
+ this.validateResponseSchemas(response, sourceFile);
23006
+ }
23007
+ validateOperationRequiredFields(operation, sourceFile) {
23008
+ if (!operation.operationId) {
23009
+ throw new MissingRequiredFieldError(
23010
+ "operation",
23011
+ "unknown",
23012
+ "operationId",
23013
+ sourceFile
23014
+ );
22878
23015
  }
22879
- if (!config?.plugins) {
22880
- return;
23016
+ if (!operation.path) {
23017
+ throw new MissingRequiredFieldError(
23018
+ "operation",
23019
+ operation.operationId,
23020
+ "path",
23021
+ sourceFile
23022
+ );
22881
23023
  }
22882
- const successful = [];
22883
- for (const plugin of config.plugins) {
22884
- let result;
22885
- if (typeof plugin === "string") {
22886
- result = await this.loadPlugin(plugin);
22887
- } else {
22888
- result = await this.loadPlugin(plugin[0]);
23024
+ if (!operation.method) {
23025
+ throw new MissingRequiredFieldError(
23026
+ "operation",
23027
+ operation.operationId,
23028
+ "method",
23029
+ sourceFile
23030
+ );
23031
+ }
23032
+ if (!operation.summary) {
23033
+ throw new MissingRequiredFieldError(
23034
+ "operation",
23035
+ operation.operationId,
23036
+ "summary",
23037
+ sourceFile
23038
+ );
23039
+ }
23040
+ }
23041
+ validateHttpMethod(operation, sourceFile) {
23042
+ const validMethods = Object.values(HttpMethod);
23043
+ if (!validMethods.includes(operation.method)) {
23044
+ throw new InvalidHttpMethodError(
23045
+ operation.operationId,
23046
+ operation.method,
23047
+ sourceFile
23048
+ );
23049
+ }
23050
+ }
23051
+ validateRequestSchemas(operation, sourceFile) {
23052
+ const request = operation.request;
23053
+ if (request.header) {
23054
+ this.validateSchema(
23055
+ request.header,
23056
+ "header",
23057
+ operation.operationId,
23058
+ "request",
23059
+ sourceFile
23060
+ );
23061
+ }
23062
+ if (request.query) {
23063
+ this.validateSchema(
23064
+ request.query,
23065
+ "query",
23066
+ operation.operationId,
23067
+ "request",
23068
+ sourceFile
23069
+ );
23070
+ }
23071
+ if (request.body) {
23072
+ this.validateSchema(
23073
+ request.body,
23074
+ "body",
23075
+ operation.operationId,
23076
+ "request",
23077
+ sourceFile
23078
+ );
23079
+ }
23080
+ if (request.param) {
23081
+ this.validateSchema(
23082
+ request.param,
23083
+ "param",
23084
+ operation.operationId,
23085
+ "request",
23086
+ sourceFile
23087
+ );
23088
+ }
23089
+ }
23090
+ validateOperationResponses(operation, sourceFile) {
23091
+ if (!operation.responses || operation.responses.length === 0) {
23092
+ throw new EmptyResponseArrayError(operation.operationId, sourceFile);
23093
+ }
23094
+ for (const response of operation.responses) {
23095
+ if (response instanceof HttpResponseDefinition) {
23096
+ continue;
22889
23097
  }
22890
- if (result.success === false) {
22891
- throw new PluginLoadingFailure(
22892
- result.error.pluginName,
22893
- result.error.attempts
23098
+ this.validateResponse(response, sourceFile);
23099
+ }
23100
+ }
23101
+ validateResponseRequiredFields(response, sourceFile) {
23102
+ if (!response.name) {
23103
+ throw new MissingRequiredFieldError(
23104
+ "response",
23105
+ "unknown",
23106
+ "name",
23107
+ sourceFile
23108
+ );
23109
+ }
23110
+ if (response.statusCode === void 0 || response.statusCode === null) {
23111
+ throw new MissingRequiredFieldError(
23112
+ "response",
23113
+ response.name,
23114
+ "statusCode",
23115
+ sourceFile
23116
+ );
23117
+ }
23118
+ if (!response.description) {
23119
+ throw new MissingRequiredFieldError(
23120
+ "response",
23121
+ response.name,
23122
+ "description",
23123
+ sourceFile
23124
+ );
23125
+ }
23126
+ }
23127
+ validateStatusCode(response, sourceFile) {
23128
+ const validStatusCodes = Object.values(HttpStatusCode);
23129
+ if (!validStatusCodes.includes(response.statusCode)) {
23130
+ throw new InvalidStatusCodeError(
23131
+ response.statusCode,
23132
+ response.name,
23133
+ sourceFile
23134
+ );
23135
+ }
23136
+ }
23137
+ validateResponseSchemas(response, sourceFile) {
23138
+ if (response.header) {
23139
+ this.validateSchema(
23140
+ response.header,
23141
+ "header",
23142
+ response.name,
23143
+ "response",
23144
+ sourceFile
23145
+ );
23146
+ }
23147
+ if (response.body) {
23148
+ this.validateSchema(
23149
+ response.body,
23150
+ "body",
23151
+ response.name,
23152
+ "response",
23153
+ sourceFile
23154
+ );
23155
+ }
23156
+ }
23157
+ validateSchema(schema, schemaType, definitionName, context, sourceFile) {
23158
+ if (schemaType === "body") {
23159
+ if (!(schema instanceof z.ZodType)) {
23160
+ throw new InvalidSchemaError(
23161
+ schemaType,
23162
+ definitionName,
23163
+ context,
23164
+ sourceFile
22894
23165
  );
22895
23166
  }
22896
- successful.push(result.value);
22897
- this.registry.register(result.value.plugin);
23167
+ return;
23168
+ }
23169
+ if (!(schema instanceof z.ZodObject)) {
23170
+ throw new InvalidSchemaError(
23171
+ schemaType,
23172
+ definitionName,
23173
+ context,
23174
+ sourceFile
23175
+ );
23176
+ }
23177
+ if (schemaType === "param") {
23178
+ this.validateParamShape(schema, definitionName, sourceFile);
23179
+ } else {
23180
+ this.validateHeaderOrQueryShape(
23181
+ schema,
23182
+ schemaType,
23183
+ definitionName,
23184
+ context,
23185
+ sourceFile
23186
+ );
22898
23187
  }
22899
- this.reportSuccessfulLoads(successful);
22900
23188
  }
22901
- /**
22902
- * Load a plugin from a string identifier
22903
- */
22904
- async loadPlugin(pluginName) {
22905
- const possiblePaths = this.generatePluginPaths(pluginName);
22906
- const attempts = [];
22907
- for (const possiblePath of possiblePaths) {
22908
- try {
22909
- const pluginPackage = await import(possiblePath);
22910
- if (pluginPackage.default) {
22911
- return {
22912
- success: true,
22913
- value: {
22914
- plugin: new pluginPackage.default(),
22915
- source: possiblePath
22916
- }
22917
- };
23189
+ validatePathParameters(operation, sourceFile) {
23190
+ const pathParamMatches = operation.path.matchAll(/:([a-zA-Z0-9_]+)/g);
23191
+ const pathParamsSet = /* @__PURE__ */ new Set();
23192
+ for (const match of pathParamMatches) {
23193
+ const paramName = match[1];
23194
+ if (pathParamsSet.has(paramName)) {
23195
+ throw new InvalidPathParameterError(
23196
+ operation.operationId,
23197
+ operation.path,
23198
+ `Duplicate parameter name '${paramName}' in path`,
23199
+ sourceFile
23200
+ );
23201
+ }
23202
+ pathParamsSet.add(paramName);
23203
+ }
23204
+ const paramSchema = operation.request?.param;
23205
+ if (pathParamsSet.size > 0 && !paramSchema) {
23206
+ throw new InvalidPathParameterError(
23207
+ operation.operationId,
23208
+ operation.path,
23209
+ `Path contains parameters [${Array.from(pathParamsSet).join(", ")}] but request.param is not defined`,
23210
+ sourceFile
23211
+ );
23212
+ }
23213
+ if (paramSchema && paramSchema instanceof z.ZodObject) {
23214
+ const paramShape = paramSchema.shape;
23215
+ const paramKeys = new Set(Object.keys(paramShape));
23216
+ for (const pathParam of pathParamsSet) {
23217
+ if (!paramKeys.has(pathParam)) {
23218
+ throw new InvalidPathParameterError(
23219
+ operation.operationId,
23220
+ operation.path,
23221
+ `Path parameter ':${pathParam}' is not defined in request.param`,
23222
+ sourceFile
23223
+ );
23224
+ }
23225
+ }
23226
+ for (const paramKey of paramKeys) {
23227
+ if (!pathParamsSet.has(paramKey)) {
23228
+ throw new InvalidPathParameterError(
23229
+ operation.operationId,
23230
+ operation.path,
23231
+ `Parameter '${paramKey}' is defined in request.param but not used in the path`,
23232
+ sourceFile
23233
+ );
22918
23234
  }
22919
- attempts.push({
22920
- path: possiblePath,
22921
- error: "No default export found"
22922
- });
22923
- } catch (error) {
22924
- attempts.push({
22925
- path: possiblePath,
22926
- error: error instanceof Error ? error.message : String(error)
22927
- });
22928
23235
  }
22929
23236
  }
22930
- return {
22931
- success: false,
22932
- error: {
22933
- pluginName,
22934
- attempts
23237
+ }
23238
+ validateHeaderOrQueryShape(schema, schemaType, definitionName, context, sourceFile) {
23239
+ const shape = schema.shape;
23240
+ for (const [propName, propSchema] of Object.entries(shape)) {
23241
+ if (!this.isValidHeaderOrQueryValue(propSchema)) {
23242
+ const typeName = this.getZodTypeName(propSchema);
23243
+ throw new InvalidSchemaShapeError(
23244
+ schemaType,
23245
+ definitionName,
23246
+ context,
23247
+ propName,
23248
+ typeName,
23249
+ sourceFile
23250
+ );
23251
+ }
23252
+ }
23253
+ }
23254
+ validateParamShape(schema, operationId, sourceFile) {
23255
+ const shape = schema.shape;
23256
+ for (const [propName, propSchema] of Object.entries(shape)) {
23257
+ if (!this.isValidParamValue(propSchema)) {
23258
+ const typeName = this.getZodTypeName(propSchema);
23259
+ throw new InvalidSchemaShapeError(
23260
+ "param",
23261
+ operationId,
23262
+ "request",
23263
+ propName,
23264
+ typeName,
23265
+ sourceFile
23266
+ );
22935
23267
  }
23268
+ }
23269
+ }
23270
+ isValidHeaderOrQueryValue(schema) {
23271
+ if (this.isStringBasedType(schema)) {
23272
+ return true;
23273
+ }
23274
+ if (schema instanceof z.ZodOptional) {
23275
+ return this.isValidHeaderOrQueryValue(schema.unwrap());
23276
+ }
23277
+ if (schema instanceof z.ZodArray) {
23278
+ return this.isStringBasedType(schema.element);
23279
+ }
23280
+ return false;
23281
+ }
23282
+ isValidParamValue(schema) {
23283
+ if (this.isStringBasedType(schema)) {
23284
+ return true;
23285
+ }
23286
+ if (schema instanceof z.ZodOptional) {
23287
+ return this.isStringBasedType(schema.unwrap());
23288
+ }
23289
+ return false;
23290
+ }
23291
+ isStringBasedType(schema) {
23292
+ if (schema instanceof z.ZodString) {
23293
+ return true;
23294
+ }
23295
+ if (schema instanceof z.ZodLiteral && typeof schema.value === "string") {
23296
+ return true;
23297
+ }
23298
+ if (schema instanceof z.ZodEnum) {
23299
+ return true;
23300
+ }
23301
+ const typeName = schema.constructor.name;
23302
+ const stringFormatTypes = [
23303
+ "ZodULID",
23304
+ "ZodUUID",
23305
+ "ZodUUIDv4",
23306
+ "ZodUUIDv7",
23307
+ "ZodUUIDv8",
23308
+ "ZodEmail",
23309
+ "ZodURL",
23310
+ "ZodCUID",
23311
+ "ZodCUID2",
23312
+ "ZodNanoID",
23313
+ "ZodBase64",
23314
+ "ZodBase64URL",
23315
+ "ZodEmoji",
23316
+ "ZodIPv4",
23317
+ "ZodIPv6",
23318
+ "ZodCIDRv4",
23319
+ "ZodCIDRv6",
23320
+ "ZodE164",
23321
+ "ZodJWT",
23322
+ "ZodASCII",
23323
+ "ZodUTF8",
23324
+ "ZodLowercase",
23325
+ "ZodGUID",
23326
+ "ZodISODate",
23327
+ "ZodISOTime",
23328
+ "ZodISODateTime",
23329
+ "ZodISODuration"
23330
+ ];
23331
+ if (stringFormatTypes.includes(typeName)) {
23332
+ return true;
23333
+ }
23334
+ if (schema._def && schema._def.typeName && schema._def.typeName.includes("String")) {
23335
+ return true;
23336
+ }
23337
+ return false;
23338
+ }
23339
+ getZodTypeName(schema) {
23340
+ const typeName = schema.constructor.name;
23341
+ if (schema instanceof z.ZodOptional) {
23342
+ return `ZodOptional<${this.getZodTypeName(schema.unwrap())}>`;
23343
+ }
23344
+ if (schema instanceof z.ZodArray) {
23345
+ return `ZodArray<${this.getZodTypeName(schema.element)}>`;
23346
+ }
23347
+ if (schema instanceof z.ZodLiteral) {
23348
+ return `ZodLiteral<${typeof schema.value}>`;
23349
+ }
23350
+ return typeName;
23351
+ }
23352
+ getRegistry() {
23353
+ return this.registry;
23354
+ }
23355
+ }
23356
+
23357
+ class InvalidSharedDirError extends Error {
23358
+ constructor(explanation) {
23359
+ super("Invalid shared dir");
23360
+ this.explanation = explanation;
23361
+ }
23362
+ }
23363
+
23364
+ class ResourceReader {
23365
+ constructor(config) {
23366
+ this.config = config;
23367
+ }
23368
+ async getResources() {
23369
+ const contents = fs.readdirSync(this.config.sourceDir, {
23370
+ withFileTypes: true
23371
+ });
23372
+ const result = {
23373
+ entityResources: {},
23374
+ sharedResponseResources: []
22936
23375
  };
23376
+ const validator = new DefinitionValidator();
23377
+ if (fs.existsSync(this.config.sharedSourceDir)) {
23378
+ const sharedStats = fs.statSync(this.config.sharedSourceDir);
23379
+ if (!sharedStats.isDirectory()) {
23380
+ throw new InvalidSharedDirError(
23381
+ `'${this.config.sharedSourceDir}' is a file, not a directory`
23382
+ );
23383
+ }
23384
+ result.sharedResponseResources = await this.getSharedResponseResources(validator);
23385
+ console.info(
23386
+ `Found '${result.sharedResponseResources.length}' shared responses in '${this.config.sharedSourceDir}'`
23387
+ );
23388
+ } else {
23389
+ console.info(
23390
+ `No shared directory found at '${this.config.sharedSourceDir}'`
23391
+ );
23392
+ }
23393
+ const normalizedSharedPath = path__default.resolve(this.config.sharedSourceDir);
23394
+ for (const content of contents) {
23395
+ if (!content.isDirectory()) {
23396
+ console.info(`Skipping '${content.name}' as it is not a directory`);
23397
+ continue;
23398
+ }
23399
+ const entityName = content.name;
23400
+ const entitySourceDir = path__default.resolve(this.config.sourceDir, entityName);
23401
+ if (entitySourceDir === normalizedSharedPath || normalizedSharedPath.startsWith(entitySourceDir + path__default.sep)) {
23402
+ console.info(
23403
+ `Skipping '${content.name}' as it is or contains the shared directory`
23404
+ );
23405
+ continue;
23406
+ }
23407
+ const responseResources = await this.getEntityResponseResources(
23408
+ entitySourceDir,
23409
+ entityName,
23410
+ validator
23411
+ );
23412
+ const operationResources = await this.getEntityOperationResources(
23413
+ entitySourceDir,
23414
+ entityName,
23415
+ validator,
23416
+ [...result.sharedResponseResources, ...responseResources]
23417
+ );
23418
+ result.entityResources[entityName] = {
23419
+ operations: operationResources,
23420
+ responses: responseResources
23421
+ };
23422
+ console.info(
23423
+ `Found '${operationResources.length}' operation definitions for entity '${entityName}'`
23424
+ );
23425
+ if (responseResources.length > 0) {
23426
+ console.info(
23427
+ `Found '${responseResources.length}' response definitions for entity '${entityName}'`
23428
+ );
23429
+ }
23430
+ }
23431
+ return result;
22937
23432
  }
22938
- /**
22939
- * Generate possible plugin paths based on configured strategies
22940
- */
22941
- generatePluginPaths(pluginName) {
22942
- const paths = [];
22943
- for (const strategy of this.strategies) {
22944
- switch (strategy) {
22945
- case "npm":
22946
- paths.push(`@rexeus/typeweaver-${pluginName}`);
22947
- paths.push(`@rexeus/${pluginName}`);
22948
- break;
22949
- case "local":
22950
- paths.push(pluginName);
22951
- break;
23433
+ scanDirectoryRecursively(dir) {
23434
+ const files = [];
23435
+ const contents = fs.readdirSync(dir, { withFileTypes: true });
23436
+ for (const content of contents) {
23437
+ const fullPath = path__default.join(dir, content.name);
23438
+ if (content.isDirectory()) {
23439
+ files.push(...this.scanDirectoryRecursively(fullPath));
23440
+ } else if (content.isFile() && content.name.endsWith(".ts")) {
23441
+ files.push(fullPath);
22952
23442
  }
22953
23443
  }
22954
- return paths;
23444
+ return files;
22955
23445
  }
22956
- /**
22957
- * Report successful plugin loads
22958
- */
22959
- reportSuccessfulLoads(successful) {
22960
- if (successful.length > 0) {
22961
- console.info(`Successfully loaded ${successful.length} plugin(s):`);
22962
- for (const result of successful) {
22963
- console.info(` - ${result.plugin.name} (from ${result.source})`);
23446
+ async getSharedResponseResources(validator) {
23447
+ const files = this.scanDirectoryRecursively(this.config.sharedSourceDir);
23448
+ const sharedResponseResources = [];
23449
+ for (const sourceFile of files) {
23450
+ const sourceFileName = path__default.basename(sourceFile);
23451
+ const definition = await import(sourceFile);
23452
+ if (!definition.default) {
23453
+ console.info(
23454
+ `Skipping '${sourceFile}' as it does not have a default export`
23455
+ );
23456
+ continue;
23457
+ }
23458
+ if (!(definition.default instanceof HttpResponseDefinition)) {
23459
+ console.info(
23460
+ `Skipping '${sourceFile}' as it is not an instance of HttpResponseDefinition`
23461
+ );
23462
+ continue;
23463
+ }
23464
+ validator.validateResponse(definition.default, sourceFile);
23465
+ const outputDir = this.config.sharedOutputDir;
23466
+ const outputFileName = `${definition.default.name}Response.ts`;
23467
+ const outputFile = path__default.join(outputDir, outputFileName);
23468
+ sharedResponseResources.push({
23469
+ ...definition.default,
23470
+ sourceDir: this.config.sharedSourceDir,
23471
+ sourceFile,
23472
+ sourceFileName,
23473
+ outputFile,
23474
+ outputFileName,
23475
+ outputDir
23476
+ });
23477
+ }
23478
+ return sharedResponseResources;
23479
+ }
23480
+ async getEntityOperationResources(sourceDir, entityName, validator, referencedResponses) {
23481
+ const files = this.scanDirectoryRecursively(sourceDir);
23482
+ const definitions = [];
23483
+ for (const sourceFile of files) {
23484
+ const sourceFileName = path__default.basename(sourceFile);
23485
+ const definition = await import(sourceFile);
23486
+ if (!definition.default) {
23487
+ console.info(
23488
+ `Skipping '${sourceFile}' as it does not have a default export`
23489
+ );
23490
+ continue;
23491
+ }
23492
+ if (!(definition.default instanceof HttpOperationDefinition)) {
23493
+ console.info(
23494
+ `Skipping '${sourceFile}' as it is not an instance of HttpOperationDefinition`
23495
+ );
23496
+ continue;
23497
+ }
23498
+ validator.validateOperation(definition.default, sourceFile);
23499
+ const { operationId } = definition.default;
23500
+ const outputDir = path__default.join(this.config.outputDir, entityName);
23501
+ const outputRequestFileName = `${operationId}Request.ts`;
23502
+ const outputRequestFile = path__default.join(outputDir, outputRequestFileName);
23503
+ const outputResponseFileName = `${operationId}Response.ts`;
23504
+ const outputResponseFile = path__default.join(outputDir, outputResponseFileName);
23505
+ const outputRequestValidationFileName = `${operationId}RequestValidator.ts`;
23506
+ const outputRequestValidationFile = path__default.join(
23507
+ outputDir,
23508
+ outputRequestValidationFileName
23509
+ );
23510
+ const outputResponseValidationFileName = `${operationId}ResponseValidator.ts`;
23511
+ const outputResponseValidationFile = path__default.join(
23512
+ outputDir,
23513
+ outputResponseValidationFileName
23514
+ );
23515
+ const outputClientFileName = `${operationId}Client.ts`;
23516
+ const outputClientFile = path__default.join(outputDir, outputClientFileName);
23517
+ const operationResource = {
23518
+ sourceDir,
23519
+ sourceFile,
23520
+ sourceFileName,
23521
+ definition: {
23522
+ ...definition.default,
23523
+ responses: []
23524
+ },
23525
+ outputDir,
23526
+ entityName,
23527
+ outputRequestFile,
23528
+ outputResponseFile,
23529
+ outputResponseValidationFile,
23530
+ outputRequestValidationFile,
23531
+ outputRequestFileName,
23532
+ outputRequestValidationFileName,
23533
+ outputResponseFileName,
23534
+ outputResponseValidationFileName,
23535
+ outputClientFile,
23536
+ outputClientFileName
23537
+ };
23538
+ if (!definition.default.responses) {
23539
+ throw new Error(
23540
+ `Operation '${operationId}' does not have any responses`
23541
+ );
23542
+ }
23543
+ for (const response of definition.default.responses) {
23544
+ if (referencedResponses.some((ref) => ref.name === response.name)) {
23545
+ const referencedResponse = {
23546
+ ...response,
23547
+ statusCodeName: HttpStatusCodeNameMap[response.statusCode],
23548
+ isReference: true
23549
+ };
23550
+ operationResource.definition.responses.push(referencedResponse);
23551
+ } else {
23552
+ const extendedResponse = {
23553
+ ...response,
23554
+ statusCodeName: HttpStatusCodeNameMap[response.statusCode],
23555
+ isReference: false
23556
+ };
23557
+ operationResource.definition.responses.push(extendedResponse);
23558
+ }
23559
+ }
23560
+ definitions.push(operationResource);
23561
+ }
23562
+ return definitions;
23563
+ }
23564
+ async getEntityResponseResources(sourceDir, entityName, validator) {
23565
+ const files = this.scanDirectoryRecursively(sourceDir);
23566
+ const responseResources = [];
23567
+ for (const sourceFile of files) {
23568
+ const sourceFileName = path__default.basename(sourceFile);
23569
+ const definition = await import(sourceFile);
23570
+ if (!definition.default) {
23571
+ continue;
23572
+ }
23573
+ if (!(definition.default instanceof HttpResponseDefinition)) {
23574
+ continue;
22964
23575
  }
23576
+ validator.validateResponse(definition.default, sourceFile);
23577
+ const outputFileName = `${definition.default.name}Response.ts`;
23578
+ const outputFile = path__default.join(
23579
+ this.config.outputDir,
23580
+ entityName,
23581
+ outputFileName
23582
+ );
23583
+ responseResources.push({
23584
+ ...definition.default,
23585
+ sourceDir,
23586
+ sourceFile,
23587
+ sourceFileName,
23588
+ outputFile,
23589
+ outputFileName,
23590
+ outputDir: path__default.join(this.config.outputDir, entityName),
23591
+ entityName
23592
+ });
22965
23593
  }
23594
+ return responseResources;
22966
23595
  }
22967
23596
  }
22968
23597
 
@@ -22976,6 +23605,8 @@ class Generator {
22976
23605
  indexFileGenerator;
22977
23606
  resourceReader = null;
22978
23607
  prettier = null;
23608
+ inputDir = "";
23609
+ sharedInputDir = "";
22979
23610
  outputDir = "";
22980
23611
  sourceDir = "";
22981
23612
  sharedSourceDir = "";
@@ -22991,13 +23622,22 @@ class Generator {
22991
23622
  */
22992
23623
  async generate(definitionDir, outputDir, config) {
22993
23624
  console.info("Starting generation...");
22994
- this.initializeDirectories(definitionDir, outputDir);
23625
+ this.initializeDirectories(definitionDir, outputDir, config?.shared);
22995
23626
  if (config?.clean ?? true) {
22996
23627
  console.info("Cleaning output directory...");
22997
23628
  fs.rmSync(this.outputDir, { recursive: true, force: true });
22998
23629
  }
22999
23630
  fs.mkdirSync(this.outputDir, { recursive: true });
23000
23631
  fs.mkdirSync(this.sharedOutputDir, { recursive: true });
23632
+ console.info(
23633
+ `Copying definitions from '${this.inputDir}' to '${this.sourceDir}'...`
23634
+ );
23635
+ fs.cpSync(this.inputDir, this.sourceDir, {
23636
+ recursive: true,
23637
+ filter: (src) => {
23638
+ return src.endsWith(".ts") || src.endsWith(".js") || src.endsWith(".json") || src.endsWith(".mjs") || src.endsWith(".cjs") || fs.statSync(src).isDirectory();
23639
+ }
23640
+ });
23001
23641
  await this.pluginLoader.loadPlugins(config);
23002
23642
  this.resourceReader = new ResourceReader({
23003
23643
  sourceDir: this.sourceDir,
@@ -23055,22 +23695,24 @@ class Generator {
23055
23695
  `Generated files: ${this.contextBuilder.getGeneratedFiles().length}`
23056
23696
  );
23057
23697
  }
23058
- initializeDirectories(definitionDir, outputDir) {
23059
- this.sourceDir = definitionDir;
23698
+ initializeDirectories(definitionDir, outputDir, sharedDir) {
23699
+ this.inputDir = definitionDir;
23700
+ this.sharedInputDir = sharedDir ?? path__default.join(definitionDir, "shared");
23060
23701
  this.outputDir = outputDir;
23061
- this.sharedSourceDir = path__default.join(definitionDir, "shared");
23062
23702
  this.sharedOutputDir = path__default.join(outputDir, "shared");
23703
+ this.sourceDir = path__default.join(this.outputDir, "definition");
23704
+ const inputToSharedDirRelative = path__default.relative(
23705
+ this.inputDir,
23706
+ this.sharedInputDir
23707
+ );
23708
+ this.sharedSourceDir = path__default.join(this.sourceDir, inputToSharedDirRelative);
23063
23709
  }
23064
23710
  }
23065
23711
 
23066
- var version = "0.0.3";
23067
- var packageJson = {
23068
- version: version};
23069
-
23070
23712
  const program = new Command();
23071
23713
  const execDir = process.cwd();
23072
23714
  program.name("@rexeus/typeweaver").description("Type-safe API framework with code generation for TypeScript").version(packageJson.version);
23073
- program.command("generate").description("Generate types, validators, and clients from API definitions").option("-i, --input <inputDir>", "path to definition directory").option("-o, --output <outputDir>", "output directory for generated files").option("-c, --config <configFile>", "path to configuration file").option("-p, --plugins <plugins>", "comma-separated list of plugins to use").option("--prettier", "format generated code with Prettier (default: true)").option("--no-prettier", "disable Prettier formatting").option("--clean", "clean output directory before generation (default: true)").option("--no-clean", "disable cleaning output directory").action(async (options) => {
23715
+ program.command("generate").description("Generate types, validators, and clients from API definitions").option("-i, --input <inputDir>", "path to definition directory").option("-o, --output <outputDir>", "output directory for generated files").option("-s, --shared <path>", "path to shared definitions directory").option("-c, --config <configFile>", "path to configuration file").option("-p, --plugins <plugins>", "comma-separated list of plugins to use").option("--prettier", "format generated code with Prettier (default: true)").option("--no-prettier", "disable Prettier formatting").option("--clean", "clean output directory before generation (default: true)").option("--no-clean", "disable cleaning output directory").action(async (options) => {
23074
23716
  let config = {};
23075
23717
  if (options.config) {
23076
23718
  const configPath = path__default.isAbsolute(options.config) ? options.config : path__default.join(execDir, options.config);
@@ -23087,6 +23729,7 @@ program.command("generate").description("Generate types, validators, and clients
23087
23729
  }
23088
23730
  const inputDir = options.input ?? config.input;
23089
23731
  const outputDir = options.output ?? config.output;
23732
+ const sharedDir = options.shared ?? config.shared;
23090
23733
  if (!inputDir) {
23091
23734
  throw new Error(
23092
23735
  "No input directory provided. Use --input or specify in config file."
@@ -23099,9 +23742,11 @@ program.command("generate").description("Generate types, validators, and clients
23099
23742
  }
23100
23743
  const resolvedInputDir = path__default.isAbsolute(inputDir) ? inputDir : path__default.join(execDir, inputDir);
23101
23744
  const resolvedOutputDir = path__default.isAbsolute(outputDir) ? outputDir : path__default.join(execDir, outputDir);
23745
+ const resolvedSharedDir = sharedDir ? path__default.isAbsolute(sharedDir) ? sharedDir : path__default.join(execDir, sharedDir) : void 0;
23102
23746
  const finalConfig = {
23103
23747
  input: resolvedInputDir,
23104
23748
  output: resolvedOutputDir,
23749
+ shared: resolvedSharedDir,
23105
23750
  prettier: options.prettier ?? config.prettier ?? true,
23106
23751
  clean: options.clean ?? config.clean ?? true
23107
23752
  };
@@ -23113,7 +23758,7 @@ program.command("generate").description("Generate types, validators, and clients
23113
23758
  const generator = new Generator();
23114
23759
  return generator.generate(resolvedInputDir, resolvedOutputDir, finalConfig);
23115
23760
  });
23116
- program.command("init").description("Initialize a new TypeWeaver project (coming soon)").action(() => {
23761
+ program.command("init").description("Initialize a new typeweaver project (coming soon)").action(() => {
23117
23762
  console.log("The init command is coming soon!");
23118
23763
  });
23119
23764
  program.parse(process.argv);