@hexaijs/plugin-contracts-generator 0.2.0 → 0.2.1

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.
package/README.md CHANGED
@@ -30,14 +30,14 @@ npm install @hexaijs/plugin-contracts-generator
30
30
  The package provides three decorators that mark messages for extraction. These decorators have **no runtime overhead** - they simply tag classes for discovery during the build process.
31
31
 
32
32
  ```typescript
33
- import { PublicEvent, PublicCommand, PublicQuery } from "@hexaijs/plugin-contracts-generator/decorators";
33
+ import { PublicEvent, PublicCommand, PublicQuery } from "@hexaijs/contracts/decorators";
34
34
  ```
35
35
 
36
36
  **@PublicEvent()** - Marks a domain event as part of the public contract:
37
37
 
38
38
  ```typescript
39
39
  import { DomainEvent } from "@hexaijs/core";
40
- import { PublicEvent } from "@hexaijs/plugin-contracts-generator/decorators";
40
+ import { PublicEvent } from "@hexaijs/contracts/decorators";
41
41
 
42
42
  @PublicEvent()
43
43
  export class OrderPlaced extends DomainEvent<{
@@ -52,7 +52,7 @@ export class OrderPlaced extends DomainEvent<{
52
52
  **@PublicCommand()** - Marks a command as part of the public contract:
53
53
 
54
54
  ```typescript
55
- import { PublicCommand } from "@hexaijs/plugin-contracts-generator/decorators";
55
+ import { PublicCommand } from "@hexaijs/contracts/decorators";
56
56
 
57
57
  @PublicCommand()
58
58
  export class CreateOrderRequest extends BaseRequest<{
@@ -70,7 +70,7 @@ export type CreateOrderResponse = {
70
70
  **@PublicQuery()** - Marks a query as part of the public contract:
71
71
 
72
72
  ```typescript
73
- import { PublicQuery } from "@hexaijs/plugin-contracts-generator/decorators";
73
+ import { PublicQuery } from "@hexaijs/contracts/decorators";
74
74
 
75
75
  @PublicQuery({ response: "OrderDetails" })
76
76
  export class GetOrderQuery extends BaseRequest<{
@@ -101,21 +101,16 @@ export default {
101
101
  contexts: [
102
102
  {
103
103
  name: "order",
104
- sourceDir: "packages/order/src",
105
- tsconfigPath: "packages/order/tsconfig.json", // optional
104
+ path: "packages/order",
105
+ tsconfigPath: "tsconfig.json", // optional, relative to path
106
106
  },
107
107
  {
108
108
  name: "inventory",
109
- sourceDir: "packages/inventory/src",
109
+ path: "packages/inventory",
110
+ sourceDir: "lib", // optional, defaults to "src"
110
111
  },
111
112
  ],
112
113
 
113
- // Output package configuration (required)
114
- outputPackage: {
115
- name: "@myorg/contracts",
116
- dir: "packages/contracts",
117
- },
118
-
119
114
  // Path alias rewrite rules (optional)
120
115
  pathAliasRewrites: {
121
116
  "@myorg/": "@/",
@@ -132,20 +127,30 @@ export default {
132
127
  { messageSuffix: "Query", responseSuffix: "QueryResult" },
133
128
  { messageSuffix: "Request", responseSuffix: "Response" },
134
129
  ],
130
+
131
+ // Custom decorator names (optional, defaults shown)
132
+ decoratorNames: {
133
+ event: "PublicEvent",
134
+ command: "PublicCommand",
135
+ query: "PublicQuery",
136
+ },
137
+
138
+ // Strip decorators from generated output (optional, default: true)
139
+ removeDecorators: true,
135
140
  },
136
141
  };
137
142
  ```
138
143
 
144
+ Each context requires `name` and `path`. The `path` is the base directory of the context (relative to the config file). Within that directory:
145
+ - `sourceDir` defaults to `"src"` (resolved relative to `path`)
146
+ - `tsconfigPath` defaults to `"tsconfig.json"` (resolved relative to `path`)
147
+
139
148
  For monorepos with many packages, use glob patterns to auto-discover contexts:
140
149
 
141
150
  ```typescript
142
151
  export default {
143
152
  contracts: {
144
153
  contexts: ["packages/*"], // Matches all directories under packages/
145
- outputPackage: {
146
- name: "@myorg/contracts",
147
- dir: "packages/contracts",
148
- },
149
154
  },
150
155
  };
151
156
  ```
@@ -208,17 +213,19 @@ The generator handles two types of files differently:
208
213
  Run the generator from your monorepo root:
209
214
 
210
215
  ```bash
211
- # Uses application.config.ts by default
212
- npx contracts-generator
216
+ # Required: --output-dir (-o) specifies where contracts are generated
217
+ npx contracts-generator --output-dir packages/contracts/src
213
218
 
214
- # Specify config file path
215
- npx contracts-generator --config ./application.config.ts
219
+ # Specify config file path (default: application.config.ts)
220
+ npx contracts-generator -o packages/contracts/src --config ./app.config.ts
216
221
 
217
222
  # Filter by message types
218
- npx contracts-generator -m event # Extract only events
219
- npx contracts-generator -m command # Extract only commands
220
- npx contracts-generator -m query # Extract only queries
221
- npx contracts-generator -m event,command # Extract events and commands
223
+ npx contracts-generator -o packages/contracts/src -m event # Extract only events
224
+ npx contracts-generator -o packages/contracts/src -m command # Extract only commands
225
+ npx contracts-generator -o packages/contracts/src -m event,command # Extract events and commands
226
+
227
+ # Generate with message registry (index.ts)
228
+ npx contracts-generator -o packages/contracts/src --generate-message-registry
222
229
  ```
223
230
 
224
231
  ### Programmatic API
@@ -255,6 +262,7 @@ contracts/
255
262
  │ ├── {context}/
256
263
  │ │ ├── events.ts
257
264
  │ │ ├── commands.ts
265
+ │ │ ├── queries.ts
258
266
  │ │ ├── types.ts # Dependent types + Response types
259
267
  │ │ └── index.ts # Barrel exports
260
268
  │ └── index.ts # Namespace exports + MessageRegistry
@@ -176,6 +176,74 @@ declare function isFunctionType(type: TypeRef): type is FunctionType;
176
176
  declare function isDomainEvent(message: Message): message is DomainEvent;
177
177
  declare function isCommand(message: Message): message is Command;
178
178
 
179
+ interface FileStats {
180
+ isDirectory(): boolean;
181
+ isFile(): boolean;
182
+ }
183
+ interface FileSystem {
184
+ readFile(path: string): Promise<string>;
185
+ readdir(path: string): Promise<string[]>;
186
+ writeFile(path: string, content: string): Promise<void>;
187
+ mkdir(path: string, options?: {
188
+ recursive?: boolean;
189
+ }): Promise<void>;
190
+ exists(path: string): Promise<boolean>;
191
+ stat(path: string): Promise<FileStats>;
192
+ }
193
+ declare class NodeFileSystem implements FileSystem {
194
+ readFile(path: string): Promise<string>;
195
+ readdir(path: string): Promise<string[]>;
196
+ writeFile(path: string, content: string): Promise<void>;
197
+ mkdir(path: string, options?: {
198
+ recursive?: boolean;
199
+ }): Promise<void>;
200
+ exists(path: string): Promise<boolean>;
201
+ stat(path: string): Promise<FileStats>;
202
+ }
203
+ declare const nodeFileSystem: NodeFileSystem;
204
+
205
+ interface InputContextConfig {
206
+ readonly name: string;
207
+ readonly path: string;
208
+ readonly sourceDir?: string;
209
+ readonly tsconfigPath?: string;
210
+ readonly responseNamingConventions?: readonly ResponseNamingConvention[];
211
+ }
212
+ /**
213
+ * Encapsulates context configuration with path resolution capabilities.
214
+ * Created via factory method to ensure proper initialization.
215
+ */
216
+ declare class ContextConfig {
217
+ private readonly fs;
218
+ private readonly tsconfig;
219
+ readonly name: string;
220
+ readonly sourceDir: string;
221
+ readonly responseNamingConventions?: readonly ResponseNamingConvention[];
222
+ private constructor();
223
+ /**
224
+ * Factory method to create ContextConfig with properly loaded tsconfig.
225
+ */
226
+ static create(input: InputContextConfig, configDir: string, fs?: FileSystem): Promise<ContextConfig>;
227
+ private static loadTsconfig;
228
+ /**
229
+ * Creates a ContextConfig without async loading (for cases where tsconfig is not needed
230
+ * or already handled externally).
231
+ */
232
+ static createSync(name: string, sourceDir: string, fs?: FileSystem, responseNamingConventions?: readonly ResponseNamingConvention[]): ContextConfig;
233
+ /**
234
+ * Resolves a module specifier (path alias) to actual file path.
235
+ * Only handles non-relative imports (path aliases).
236
+ *
237
+ * @param moduleSpecifier - The import path to resolve (e.g., "@/utils/helper")
238
+ * @returns Object with resolvedPath (null if external) and isExternal flag
239
+ */
240
+ resolvePath(moduleSpecifier: string): Promise<{
241
+ resolvedPath: string | null;
242
+ isExternal: boolean;
243
+ }>;
244
+ private tryResolveWithExtensions;
245
+ }
246
+
179
247
  /**
180
248
  * Options for runWithConfig when config is provided directly.
181
249
  */
@@ -189,12 +257,7 @@ interface RunWithConfigOptions {
189
257
  * This is the config passed from hexai.config.ts.
190
258
  */
191
259
  interface ContractsPluginConfig {
192
- contexts: Array<{
193
- name: string;
194
- path: string;
195
- sourceDir?: string;
196
- tsconfigPath?: string;
197
- }>;
260
+ contexts: Array<string | InputContextConfig>;
198
261
  pathAliasRewrites?: Record<string, string>;
199
262
  externalDependencies?: Record<string, string>;
200
263
  decoratorNames?: DecoratorNames;
@@ -211,4 +274,4 @@ declare function run(args: string[]): Promise<void>;
211
274
  */
212
275
  declare function runWithConfig(options: RunWithConfigOptions, pluginConfig: ContractsPluginConfig): Promise<void>;
213
276
 
214
- export { type ArrayType as A, isObjectType as B, type Command as C, type DecoratorNames as D, type EnumDefinition as E, type Field as F, isPrimitiveType as G, isReferenceType as H, type ImportSource as I, isTupleType as J, isUnionType as K, type LiteralType as L, type MessageType as M, type RunWithConfigOptions as N, type ObjectType as O, type PrimitiveType as P, type Query as Q, type ResponseNamingConvention as R, type SourceFile as S, type TypeDefinition as T, type UnionType as U, run as V, runWithConfig as W, type DomainEvent as a, type ContractsPluginConfig as b, type ClassDefinition as c, type ClassImport as d, type Config as e, type Dependency as f, type DependencyKind as g, type EnumMember as h, type ExtractionError as i, type ExtractionResult as j, type ExtractionWarning as k, type FunctionParameter as l, type FunctionType as m, type IntersectionType as n, type Message as o, type MessageBase as p, type ReferenceType as q, type TupleType as r, type TypeDefinitionKind as s, type TypeRef as t, isArrayType as u, isCommand as v, isDomainEvent as w, isFunctionType as x, isIntersectionType as y, isLiteralType as z };
277
+ export { runWithConfig as $, type ArrayType as A, isDomainEvent as B, type Command as C, type DecoratorNames as D, type EnumDefinition as E, type FileSystem as F, isFunctionType as G, isIntersectionType as H, type InputContextConfig as I, isLiteralType as J, isObjectType as K, type LiteralType as L, type MessageType as M, isPrimitiveType as N, type ObjectType as O, type PrimitiveType as P, type Query as Q, type ResponseNamingConvention as R, type SourceFile as S, type TypeDefinition as T, type UnionType as U, isReferenceType as V, isTupleType as W, isUnionType as X, nodeFileSystem as Y, type RunWithConfigOptions as Z, run as _, type DomainEvent as a, ContextConfig as b, type ContractsPluginConfig as c, type ClassDefinition as d, type ClassImport as e, type Config as f, type Dependency as g, type DependencyKind as h, type EnumMember as i, type ExtractionError as j, type ExtractionResult as k, type ExtractionWarning as l, type Field as m, type FileStats as n, type FunctionParameter as o, type FunctionType as p, type ImportSource as q, type IntersectionType as r, type Message as s, type MessageBase as t, type ReferenceType as u, type TupleType as v, type TypeDefinitionKind as w, type TypeRef as x, isArrayType as y, isCommand as z };
package/dist/cli.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  #!/usr/bin/env node
2
- export { b as ContractsPluginConfig, N as RunWithConfigOptions, V as run, W as runWithConfig } from './cli-CPg-O4OY.js';
2
+ export { c as ContractsPluginConfig, Z as RunWithConfigOptions, _ as run, $ as runWithConfig } from './cli-DajurpEQ.js';
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { fileURLToPath } from 'url';
3
3
  import * as path from 'path';
4
- import path__default, { resolve, dirname, basename, relative, join } from 'path';
4
+ import path__default, { resolve, dirname, join, relative, basename } from 'path';
5
5
  import * as ts8 from 'typescript';
6
6
  import ts8__default from 'typescript';
7
7
  import { readFile, readdir, writeFile, mkdir, access, stat } from 'fs/promises';
8
8
  import { constants } from 'fs';
9
- import 'reflect-metadata';
9
+ import '@hexaijs/contracts/decorators';
10
10
  import { glob } from 'glob';
11
11
  import { minimatch } from 'minimatch';
12
12
 
@@ -233,6 +233,90 @@ var ContextConfig = class _ContextConfig {
233
233
 
234
234
  // src/config-loader.ts
235
235
  var SUPPORTED_GLOB_PARTS_COUNT = 2;
236
+ async function resolveContextEntries(entries, configDir, fs = nodeFileSystem) {
237
+ const contexts = [];
238
+ for (let i = 0; i < entries.length; i++) {
239
+ const item = entries[i];
240
+ if (typeof item === "string") {
241
+ const resolved = await resolveStringEntry(item, configDir, fs);
242
+ contexts.push(...resolved);
243
+ } else {
244
+ const contextConfig = await createObjectContext(item, i, configDir, fs);
245
+ contexts.push(contextConfig);
246
+ }
247
+ }
248
+ return contexts;
249
+ }
250
+ async function resolveStringEntry(contextPath, configDir, fs) {
251
+ if (contextPath.includes("*")) {
252
+ return expandGlobPattern(contextPath, configDir, fs);
253
+ }
254
+ const basePath = resolve(configDir, contextPath);
255
+ const name = basename(basePath);
256
+ return [await ContextConfig.create(
257
+ { name, path: contextPath },
258
+ configDir,
259
+ fs
260
+ )];
261
+ }
262
+ async function expandGlobPattern(pattern, configDir, fs) {
263
+ const packageDirs = await matchGlobPattern(pattern, configDir, fs);
264
+ return Promise.all(
265
+ packageDirs.map((dir) => {
266
+ const name = basename(dir);
267
+ const relativePath = relative(configDir, dir);
268
+ return ContextConfig.create(
269
+ { name, path: relativePath },
270
+ configDir,
271
+ fs
272
+ );
273
+ })
274
+ );
275
+ }
276
+ async function createObjectContext(ctx, index, configDir, fs) {
277
+ if (!ctx.name || typeof ctx.name !== "string") {
278
+ throw new ConfigLoadError(
279
+ `Invalid context at index ${index}: missing 'name'`
280
+ );
281
+ }
282
+ if (!ctx.path || typeof ctx.path !== "string") {
283
+ throw new ConfigLoadError(
284
+ `Invalid context at index ${index}: missing 'path'`
285
+ );
286
+ }
287
+ return ContextConfig.create(ctx, configDir, fs);
288
+ }
289
+ async function matchGlobPattern(pattern, configDir, fs) {
290
+ const globParts = pattern.split("*");
291
+ if (globParts.length !== SUPPORTED_GLOB_PARTS_COUNT) {
292
+ throw new ConfigLoadError(
293
+ `Invalid glob pattern: "${pattern}". Only single wildcard patterns like "packages/*" are supported.`
294
+ );
295
+ }
296
+ const [prefix, suffix] = globParts;
297
+ const baseDir = resolve(configDir, prefix);
298
+ if (!await fs.exists(baseDir)) {
299
+ return [];
300
+ }
301
+ const entries = await fs.readdir(baseDir);
302
+ const matchedDirs = [];
303
+ for (const entry of entries) {
304
+ const fullPath = resolve(baseDir, entry);
305
+ const stats = await fs.stat(fullPath);
306
+ if (!stats.isDirectory()) {
307
+ continue;
308
+ }
309
+ if (suffix) {
310
+ const suffixPath = resolve(fullPath, suffix.replace(/^\//, ""));
311
+ if (await fs.exists(suffixPath)) {
312
+ matchedDirs.push(fullPath);
313
+ }
314
+ } else {
315
+ matchedDirs.push(fullPath);
316
+ }
317
+ }
318
+ return matchedDirs.sort();
319
+ }
236
320
  var ConfigLoader = class {
237
321
  fs;
238
322
  constructor(options = {}) {
@@ -265,7 +349,7 @@ var ConfigLoader = class {
265
349
  if (!contracts.contexts || !Array.isArray(contracts.contexts)) {
266
350
  throw new ConfigLoadError("Missing 'contracts.contexts' in config");
267
351
  }
268
- const contexts = await this.resolveContexts(contracts.contexts, configDir);
352
+ const contexts = await resolveContextEntries(contracts.contexts, configDir, this.fs);
269
353
  if (contexts.length === 0) {
270
354
  throw new ConfigLoadError("No contexts found from 'contexts'");
271
355
  }
@@ -279,90 +363,6 @@ var ConfigLoader = class {
279
363
  removeDecorators: contracts.removeDecorators ?? true
280
364
  };
281
365
  }
282
- async resolveContexts(contextsConfig, configDir) {
283
- const contexts = [];
284
- for (let i = 0; i < contextsConfig.length; i++) {
285
- const item = contextsConfig[i];
286
- if (typeof item === "string") {
287
- const resolvedContexts = await this.resolveStringContext(item, configDir);
288
- contexts.push(...resolvedContexts);
289
- } else {
290
- const contextConfig = await this.createObjectContext(item, i, configDir);
291
- contexts.push(contextConfig);
292
- }
293
- }
294
- return contexts;
295
- }
296
- async resolveStringContext(contextPath, configDir) {
297
- if (contextPath.includes("*")) {
298
- return this.expandGlobPattern(contextPath, configDir);
299
- }
300
- const basePath = resolve(configDir, contextPath);
301
- const name = basename(basePath);
302
- return [await ContextConfig.create(
303
- { name, path: contextPath },
304
- configDir,
305
- this.fs
306
- )];
307
- }
308
- async expandGlobPattern(pattern, configDir) {
309
- const packageDirs = await this.matchGlobPattern(pattern, configDir);
310
- return Promise.all(
311
- packageDirs.map((dir) => {
312
- const name = basename(dir);
313
- const relativePath = relative(configDir, dir);
314
- return ContextConfig.create(
315
- { name, path: relativePath },
316
- configDir,
317
- this.fs
318
- );
319
- })
320
- );
321
- }
322
- async createObjectContext(ctx, index, configDir) {
323
- if (!ctx.name || typeof ctx.name !== "string") {
324
- throw new ConfigLoadError(
325
- `Invalid context at index ${index}: missing 'name'`
326
- );
327
- }
328
- if (!ctx.path || typeof ctx.path !== "string") {
329
- throw new ConfigLoadError(
330
- `Invalid context at index ${index}: missing 'path'`
331
- );
332
- }
333
- return ContextConfig.create(ctx, configDir, this.fs);
334
- }
335
- async matchGlobPattern(pattern, configDir) {
336
- const globParts = pattern.split("*");
337
- if (globParts.length !== SUPPORTED_GLOB_PARTS_COUNT) {
338
- throw new ConfigLoadError(
339
- `Invalid glob pattern: "${pattern}". Only single wildcard patterns like "packages/*" are supported.`
340
- );
341
- }
342
- const [prefix, suffix] = globParts;
343
- const baseDir = resolve(configDir, prefix);
344
- if (!await this.fs.exists(baseDir)) {
345
- return [];
346
- }
347
- const entries = await this.fs.readdir(baseDir);
348
- const matchedDirs = [];
349
- for (const entry of entries) {
350
- const fullPath = resolve(baseDir, entry);
351
- const stats = await this.fs.stat(fullPath);
352
- if (!stats.isDirectory()) {
353
- continue;
354
- }
355
- if (suffix) {
356
- const suffixPath = resolve(fullPath, suffix.replace(/^\//, ""));
357
- if (await this.fs.exists(suffixPath)) {
358
- matchedDirs.push(fullPath);
359
- }
360
- } else {
361
- matchedDirs.push(fullPath);
362
- }
363
- }
364
- return matchedDirs.sort();
365
- }
366
366
  };
367
367
  var DEFAULT_EXCLUDE_PATTERNS = [
368
368
  "**/node_modules/**",
@@ -2360,9 +2360,12 @@ Config file format:
2360
2360
  export default {
2361
2361
  contracts: {
2362
2362
  contexts: [
2363
+ "packages/*", // glob: auto-discover all contexts
2364
+ "packages/auth", // string: name inferred from directory
2363
2365
  {
2364
2366
  name: "lecture",
2365
- sourceDir: "packages/lecture/src",
2367
+ path: "packages/lecture", // required: base directory
2368
+ sourceDir: "src", // optional, default: "src"
2366
2369
  },
2367
2370
  ],
2368
2371
  pathAliasRewrites: {
@@ -2448,19 +2451,10 @@ async function run(args) {
2448
2451
  }
2449
2452
  }
2450
2453
  async function toContractsConfig(pluginConfig) {
2451
- const contexts = await Promise.all(
2452
- pluginConfig.contexts.map(
2453
- (ctx) => ContextConfig.create(
2454
- {
2455
- name: ctx.name,
2456
- path: ctx.path,
2457
- sourceDir: ctx.sourceDir,
2458
- tsconfigPath: ctx.tsconfigPath
2459
- },
2460
- process.cwd(),
2461
- nodeFileSystem
2462
- )
2463
- )
2454
+ const contexts = await resolveContextEntries(
2455
+ pluginConfig.contexts,
2456
+ process.cwd(),
2457
+ nodeFileSystem
2464
2458
  );
2465
2459
  return {
2466
2460
  contexts,