@mnemonica/tactica 0.1.4 → 0.1.5

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
@@ -221,7 +221,124 @@ const AdminType = UserType.define('AdminType', function (this: { role: string })
221
221
 
222
222
  Chained calls (`define('A').define('B')`) and nested calls via variable references (`const A = define('A', …); A.define('B', …)`) are both supported.
223
223
 
224
- ### `@decorate()` decorator
224
+ ### Builder pattern on the imported module object
225
+
226
+ The analyzer also recognizes the chainable `mnemonica` module object and aliases of it:
227
+
228
+ ```ts
229
+ import { mnemonica } from 'mnemonica';
230
+
231
+ const App = mnemonica
232
+ .define('UserType', function (this: { name: string }) {
233
+ this.name = '';
234
+ })
235
+ .define('AdminType', function (this: { role: string }) {
236
+ this.role = 'admin';
237
+ });
238
+
239
+ // Builder .lookup() results are followed
240
+ App.lookup('UserType').define('GuestType', function (this: { token: string }) {
241
+ this.token = '';
242
+ });
243
+ ```
244
+
245
+ It also recognizes the explicit-source APIs:
246
+
247
+ ```ts
248
+ import { define, lookup } from 'mnemonica';
249
+
250
+ const AdminType = define(UserType, 'AdminType', function (this: { role: string }) {
251
+ this.role = 'admin';
252
+ });
253
+
254
+ const AdminCtor = lookup(App, 'UserType.AdminType');
255
+ ```
256
+
257
+ ### Custom collections via `createTypesCollection()`
258
+
259
+ Types defined on a collection created with `createTypesCollection()` are tracked by the analyzer. By default they are **not** emitted in `.tactica/types.ts` and are **not** added to the global `mnemonica.TypeRegistry` augmentation because they live in an isolated runtime collection. Use the collection variable directly in your own code to get typed constructors.
260
+
261
+ All common import styles are recognized:
262
+
263
+ ```ts
264
+ import { createTypesCollection } from 'mnemonica';
265
+
266
+ const AppCollection = createTypesCollection();
267
+
268
+ AppCollection.define('UserType', function (this: { name: string }) {
269
+ this.name = '';
270
+ });
271
+ ```
272
+
273
+ ```ts
274
+ import { mnemonica } from 'mnemonica';
275
+
276
+ const AppCollection = mnemonica.createTypesCollection();
277
+ ```
278
+
279
+ ```ts
280
+ import * as mnemonica from 'mnemonica';
281
+
282
+ const AppCollection = mnemonica.createTypesCollection();
283
+ ```
284
+
285
+ ```ts
286
+ import { createTypesCollection as ctc } from 'mnemonica';
287
+
288
+ const AppCollection = ctc();
289
+ ```
290
+
291
+ Subtypes defined from a collection type inherit the same collection membership:
292
+
293
+ ```ts
294
+ const UserType = AppCollection.define('UserType', function (this: { name: string }) {
295
+ this.name = '';
296
+ });
297
+
298
+ UserType.define('AdminType', function (this: { role: string }) {
299
+ this.role = 'admin';
300
+ });
301
+ ```
302
+
303
+ Two independent collections may define root types with the same name without colliding in the analyzer graph.
304
+
305
+ #### Option B — user-provided registry interface
306
+
307
+ To get fully typed `.lookup()` and `.decorate()` for a custom collection, export an empty interface and pass it to `createTypesCollection<Registry>()`:
308
+
309
+ ```ts
310
+ import { createTypesCollection } from 'mnemonica';
311
+
312
+ export interface AppCollectionRegistry {}
313
+
314
+ const AppCollection = createTypesCollection<AppCollectionRegistry>();
315
+
316
+ const UserType = AppCollection.define('UserType', function (this: { name: string }) {
317
+ this.name = '';
318
+ });
319
+
320
+ UserType.define('AdminType', function (this: { role: string }) {
321
+ this.role = 'admin';
322
+ });
323
+ ```
324
+
325
+ Tactica then emits:
326
+
327
+ - Prefixed instance types in `.tactica/types.ts`, e.g. `AppCollectionRegistry_UserType` and `AppCollectionRegistry_UserType_AdminType`.
328
+ - A `declare module '<relative path to this file>'` block in `.tactica/registry.ts` that augments `AppCollectionRegistry` with `'UserType'` and `'UserType.AdminType'` entries.
329
+
330
+ Once `.tactica/registry.ts` is part of your `tsc` compilation, `AppCollection.lookup('UserType')` and `AppCollection.lookup('UserType.AdminType')` are fully typed.
331
+
332
+ Custom collections also support `decorate()` for root types:
333
+
334
+ ```ts
335
+ @AppCollection.decorate()
336
+ class UserType {
337
+ name: string = '';
338
+ }
339
+ ```
340
+
341
+ Decorated root types are included in the same per-collection augmentation.
225
342
 
226
343
  ```ts
227
344
  @decorate()
@@ -387,7 +504,7 @@ class TypeGraphImpl implements TypeGraph {
387
504
  *bfs(): Generator<TypeNode>;
388
505
  *dfs(node?: TypeNode): Generator<TypeNode>;
389
506
 
390
- static createNode(name, parent, sourceFile, line, column): TypeNode;
507
+ static createNode(name, parent, sourceFile, line, column, collectionId?): TypeNode;
391
508
  }
392
509
  ```
393
510
 
@@ -523,6 +640,10 @@ Mnemonica accepts the `exposeInstanceMethods` option at runtime, but tactica's `
523
640
 
524
641
  The analyzer does not use `ts.Program.getTypeChecker()` for resolution, so cross-file type references in unusual shapes may resolve to `unknown`.
525
642
 
643
+ ### Custom collection name conflicts
644
+
645
+ Custom collection types are isolated in the analyzer by their collection identity, so two collections may both define a root type with the same name (e.g., both defining `'User'`) without overwriting each other. Because custom-collection types are not emitted in `.tactica/types.ts`, they also do not conflict in generated output.
646
+
526
647
  ## Troubleshooting
527
648
 
528
649
  **Generated types are missing properties** — make sure the constructor function has either an explicit `this:` type annotation, or that the data parameter has an inline / aliased type (`{ id: string }` or `Foo` where `type Foo = { id: string }`). Property inference walks one level of indirection.
package/lib/analyzer.d.ts CHANGED
@@ -13,6 +13,11 @@ export declare class MnemonicaAnalyzer {
13
13
  private flowUsages;
14
14
  private typeAliases;
15
15
  private variableToTypeMap;
16
+ private moduleObjectVariables;
17
+ private createTypesCollectionVariables;
18
+ private collectionVariables;
19
+ private collectionInfo;
20
+ private collectionCounter;
16
21
  constructor(program?: ts.Program);
17
22
  /**
18
23
  * Reset usage-related state for a fresh pass.
@@ -60,10 +65,57 @@ export declare class MnemonicaAnalyzer {
60
65
  * Visit a node in the AST
61
66
  */
62
67
  private visitNode;
68
+ /**
69
+ * Track imports from 'mnemonica' so aliases of the module object and
70
+ * createTypesCollection are recognized without relying on the type checker.
71
+ */
72
+ private trackImports;
73
+ /**
74
+ * Track aliases of the mnemonica module object, e.g.:
75
+ * const m = mnemonica;
76
+ * const App = m;
77
+ */
78
+ private trackModuleObjectAliases;
79
+ /**
80
+ * Track custom collection variables, e.g.:
81
+ * const MyCollection = createTypesCollection();
82
+ * const Other = MyCollection;
83
+ *
84
+ * Also detects Option B user-provided registry interfaces:
85
+ * export interface MyCollectionRegistry {}
86
+ * const MyCollection = createTypesCollection<MyCollectionRegistry>();
87
+ */
88
+ private trackCollectionAliases;
89
+ /**
90
+ * Extract the registry interface name from createTypesCollection<Registry>()
91
+ * when the interface is declared in the same source file.
92
+ */
93
+ private extractRegistryInterfaceName;
94
+ /**
95
+ * Get the registry interface name for a collection id.
96
+ */
97
+ private getRegistryInterfaceName;
98
+ /**
99
+ * Check if an expression is a createTypesCollection() call.
100
+ * Handles:
101
+ * createTypesCollection()
102
+ * ctc() // aliased import
103
+ * mnemonica.createTypesCollection() // module object method
104
+ * m.createTypesCollection() // aliased module object
105
+ */
106
+ private isCreateTypesCollectionCall;
107
+ /**
108
+ * Generate a unique collection identifier.
109
+ */
110
+ private nextCollectionId;
63
111
  /**
64
112
  * Check if a node is a define() call
65
113
  */
66
114
  private isDefineCall;
115
+ /**
116
+ * Check if a node is a lazy() call
117
+ */
118
+ private isLazyCall;
67
119
  /**
68
120
  * Extract config options from an object literal
69
121
  */
@@ -76,10 +128,52 @@ export declare class MnemonicaAnalyzer {
76
128
  * Check if a node is a @decorate() decorator
77
129
  */
78
130
  private isDecorateDecorator;
131
+ /**
132
+ * Mark a call expression as processed and return whether it already was.
133
+ */
134
+ private markProcessed;
79
135
  /**
80
136
  * Process a define() call
81
137
  */
82
138
  private processDefineCall;
139
+ /**
140
+ * Process a lazy() call
141
+ */
142
+ private processLazyCall;
143
+ /**
144
+ * Extract lazy() call arguments into a normalized shape.
145
+ * Handles named/unnamed and explicit-source forms, both as free calls
146
+ * and as method calls.
147
+ */
148
+ private extractLazyCallArgs;
149
+ /**
150
+ * Unwrap the constructor returned by a lazy getter.
151
+ * Supports:
152
+ * () => class Name {}
153
+ * () => function Name() {}
154
+ * () => { return class Name {}; }
155
+ * function () { return function Name() {}; }
156
+ */
157
+ private unwrapLazyGetter;
158
+ /**
159
+ * Extract a constructor name from a class expression, class declaration,
160
+ * or named function expression.
161
+ */
162
+ private extractConstructorName;
163
+ /**
164
+ * Extract the type name from either a define() or lazy() call.
165
+ */
166
+ private extractMnemonicaTypeName;
167
+ /**
168
+ * Extract the full lazy() call context: type name, parent type, and collection.
169
+ * Handles direct calls, property-access calls, chained calls, and the
170
+ * explicit-source form `lazy(source, 'TypeName', getter)`.
171
+ */
172
+ private extractLazyContext;
173
+ /**
174
+ * Extract config options from lazy() call
175
+ */
176
+ private extractLazyConfig;
83
177
  /**
84
178
  * Track variable assignments that capture define() results
85
179
  * e.g., const User = define('UserEntity', ...) maps "User" -> "UserEntity"
@@ -101,15 +195,46 @@ export declare class MnemonicaAnalyzer {
101
195
  */
102
196
  private processDecorateDecorator;
103
197
  /**
104
- * Extract type name from define() call arguments
198
+ * Extract type name from define() call arguments.
199
+ * Handles:
200
+ * define('TypeName', handler)
201
+ * define(source, 'TypeName', handler) // explicit-source form
202
+ * define(function TypeName() {})
203
+ * define(() => class TypeName {})
105
204
  */
106
205
  private extractTypeName;
107
206
  /**
108
- * Find parent type node for nested define calls
207
+ * Extract the full define() call context: type name, parent type, and collection.
208
+ * Handles direct calls, property-access calls, chained calls, and the
209
+ * explicit-source form `define(source, 'TypeName', handler)`.
210
+ */
211
+ private extractDefineContext;
212
+ /**
213
+ * Prefix a dotted type path with a collection identifier so custom-collection
214
+ * types do not collide with default-collection types in the graph.
215
+ */
216
+ private prefixCollectionPath;
217
+ /**
218
+ * Resolve a define() source identifier to either a parent type, a collection,
219
+ * or the default (module object) collection.
220
+ */
221
+ private resolveDefineSource;
222
+ /**
223
+ * Check if a call expression is a lookup() call.
109
224
  */
110
- private findParentType;
225
+ private isLookupCall;
111
226
  /**
112
- * Find a parent type by its name, searching in the graph
227
+ * Resolve a lookup() call to a dotted type path (best effort).
228
+ * Handles:
229
+ * lookup('User')
230
+ * lookup(source, 'User')
231
+ * App.lookup('User')
232
+ * collection.lookup('User.Admin')
233
+ */
234
+ private resolveLookupPath;
235
+ /**
236
+ * Find a parent type by its name, searching in the graph.
237
+ * When collectionId is provided, only types from that collection are considered.
113
238
  */
114
239
  private findParentTypeByName;
115
240
  /**
@@ -118,14 +243,29 @@ export declare class MnemonicaAnalyzer {
118
243
  * and direct class/type names.
119
244
  */
120
245
  private findParentTypeByIdentifier;
246
+ /**
247
+ * Get the leftmost identifier of a property-access chain.
248
+ * For `App.define('User').define('Admin')` this returns the `App` identifier.
249
+ */
250
+ private getRootIdentifier;
121
251
  /**
122
252
  * Get property chain from nested access
123
253
  */
124
254
  private getPropertyChain;
255
+ /**
256
+ * Determine the constructor expression for either a define() or lazy() call.
257
+ * For define() this is the construct handler; for lazy() it is the value
258
+ * returned by the lazy getter.
259
+ */
260
+ private extractConstructorExpression;
125
261
  /**
126
262
  * Extract properties from constructor function
127
263
  */
128
264
  private extractProperties;
265
+ /**
266
+ * Extract properties from a constructor expression (function, arrow, or class).
267
+ */
268
+ private extractPropertiesFromConstructor;
129
269
  /**
130
270
  * Build a type map from all parameters with inline object type annotations
131
271
  * Returns a map of "paramName.propertyName" -> type
@@ -184,16 +324,16 @@ export declare class MnemonicaAnalyzer {
184
324
  */
185
325
  private inferTypeFromInitializer;
186
326
  /**
187
- * Collect usage information for type references
188
- */
327
+ * Collect usage information for type references
328
+ */
189
329
  private collectUsage;
190
330
  /**
191
- * Get function name from expression (identifier or property access)
192
- */
331
+ * Get function name from expression (identifier or property access)
332
+ */
193
333
  private getFunctionName;
194
334
  /**
195
- * Add a usage to the collection
196
- */
335
+ * Add a usage to the collection
336
+ */
197
337
  private addUsage;
198
338
  /**
199
339
  * Collect EDS (Execution Data Storage) usage information
@@ -257,26 +397,30 @@ export declare class MnemonicaAnalyzer {
257
397
  */
258
398
  private getTypeNameFromExpression;
259
399
  /**
260
- * Resolve full type path from property access
261
- */
400
+ * Resolve full type path from property access
401
+ */
262
402
  private resolveTypePath;
263
403
  /**
264
- * Check if a name looks like a type (starts with uppercase)
265
- */
404
+ * Check if a name looks like a type (starts with uppercase)
405
+ */
266
406
  private isLikelyTypeName;
267
407
  /**
268
- * Resolve a constructor parameter type, expanding inline object literals
269
- * and type aliases where possible.
270
- */
408
+ * Resolve a constructor parameter type, expanding inline object literals
409
+ * and type aliases where possible.
410
+ */
271
411
  private resolveConstructorParamType;
272
412
  /**
273
- * Extract constructor parameters from a class-like node.
274
- */
413
+ * Extract constructor parameters from a class-like node.
414
+ */
275
415
  private extractClassConstructorParams;
276
416
  /**
277
- * Extract constructor parameters from define() call
278
- * This is used for TypeRegistry constructor signatures
279
- * Preserves parameter names and expands object types to their structure
280
- */
417
+ * Extract constructor parameters from define() call
418
+ * This is used for TypeRegistry constructor signatures
419
+ * Preserves parameter names and expands object types to their structure
420
+ */
281
421
  private extractConstructorParams;
422
+ /**
423
+ * Extract constructor parameters from a constructor expression.
424
+ */
425
+ private extractConstructorParamsFromConstructor;
282
426
  }