@mnemonica/tactica 0.4.9 → 0.5.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.
- package/README.md +111 -44
- package/SKILL.md +37 -22
- package/lib/analyzer.d.ts +31 -2
- package/lib/analyzer.js +511 -37
- package/lib/cli.js +13 -42
- package/lib/generator.d.ts +11 -25
- package/lib/generator.js +194 -165
- package/lib/types.d.ts +16 -2
- package/lib/types.js +1 -1
- package/lib/writer.d.ts +0 -4
- package/lib/writer.js +1 -10
- package/package.json +20 -4
package/README.md
CHANGED
|
@@ -88,9 +88,6 @@ const writer = new TypesWriter('.tactica');
|
|
|
88
88
|
// Default mode — exportable type aliases in types.ts + a TypeRegistry augmentation in registry.ts.
|
|
89
89
|
writer.writeTypesFile(generator.generateTypesFile());
|
|
90
90
|
writer.writeTo('registry.ts', generator.generateTypeRegistry().content);
|
|
91
|
-
|
|
92
|
-
// Legacy mode — single global augmentation file index.d.ts.
|
|
93
|
-
// writer.writeGlobalAugmentation(generator.generateGlobalAugmentation());
|
|
94
91
|
```
|
|
95
92
|
|
|
96
93
|
### Project setup
|
|
@@ -122,7 +119,6 @@ You should commit `.tactica/` if you want type information to flow through CI wi
|
|
|
122
119
|
| `--include <patterns>` | `-i` | Comma-separated include patterns |
|
|
123
120
|
| `--exclude <patterns>` | `-e` | Comma-separated exclude patterns |
|
|
124
121
|
| `--topologica <dirs>` | `-t` | Comma-separated Topologica directories to scan |
|
|
125
|
-
| `--module-augmentation` | `-m` | Legacy mode — emit a single global-augmentation `index.d.ts` instead of `types.ts` + `registry.ts` |
|
|
126
122
|
| `--esm` | | Append `.js` extensions to relative imports in generated files (for ESM / NodeNext module resolution) |
|
|
127
123
|
| `--eds` | | Force-enable EDS (Execution Data Storage) tracking |
|
|
128
124
|
| `--no-eds` | | Force-disable EDS tracking |
|
|
@@ -142,7 +138,6 @@ Tactica writes everything under the `--output` directory (default `.tactica/`).
|
|
|
142
138
|
| `types.ts` | default mode | Exportable type aliases — one per discovered type. Uses `ProtoFlat<Parent, Self>` for nested types. |
|
|
143
139
|
| `registry.ts` | default mode | `declare module 'mnemonica' { interface TypeRegistry { … } }` augmentation — powers `lookup<K>()`. |
|
|
144
140
|
| `index.ts` | default mode | Re-exports everything from `types.ts` and `registry.ts`. |
|
|
145
|
-
| `index.d.ts` | with `--module-augmentation` | Single global-augmentation file (legacy mode). |
|
|
146
141
|
| `definitions.json` | always | One entry per discovered type: `{ name, location, kind: 'define'\|'decorate', parent, strictChain, blockErrors }`, plus `fields` (the type's own fields in declaration order, `{ name, type, optional }`) and `args` (the constructor's parameters in declaration order, `{ name, type, optional?, kind? }` — a rest parameter carries `kind: 'rest'` and no `optional`). Consumed by `mnemographica`'s Go to Definition. |
|
|
147
142
|
| `usages.json` | always | One entry per type, value is an array of `{ location, kind, code, holderScopeId?, constructorText? }` records — where each type is instantiated, referenced, accessed, or looked up. `holderScopeId` points into `scopes.json` (the innermost scope holding the usage); `constructorText` (instantiations only) is the constructor expression text actually used. Consumed by `mnemographica`'s Find References. |
|
|
148
143
|
| `flow.json` | always | Native instance-flow patterns (property reads/writes, method calls, destructuring, returns, spreads, etc.) per type. |
|
|
@@ -155,32 +150,49 @@ Tactica writes everything under the `--output` directory (default `.tactica/`).
|
|
|
155
150
|
### Default mode (types.ts + registry.ts)
|
|
156
151
|
|
|
157
152
|
```ts
|
|
158
|
-
// .tactica/
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
email: string;
|
|
162
|
-
AdminType: new (data: { role: string }) => UserType_AdminType;
|
|
163
|
-
};
|
|
153
|
+
// .tactica/registry.ts
|
|
154
|
+
import type { App } from '../src/app';
|
|
155
|
+
type FromBuilder = RegistryOf<typeof App>;
|
|
164
156
|
|
|
165
|
-
|
|
166
|
-
role: string;
|
|
167
|
-
AdminType: undefined;
|
|
168
|
-
}>;
|
|
169
|
-
```
|
|
157
|
+
import type { Gadget, Gadget_Part } from './types';
|
|
170
158
|
|
|
171
|
-
```ts
|
|
172
|
-
// .tactica/registry.ts
|
|
173
159
|
declare module 'mnemonica' {
|
|
174
|
-
interface TypeRegistry {
|
|
175
|
-
'
|
|
176
|
-
'
|
|
160
|
+
interface TypeRegistry extends FromBuilder {
|
|
161
|
+
'Gadget': RegistryEntry<Gadget, 'Gadget', object, [g: string]>;
|
|
162
|
+
'Gadget.Part': RegistryEntry<Gadget_Part, 'Gadget.Part', Gadget, [data: { size: number }]>;
|
|
177
163
|
}
|
|
178
164
|
}
|
|
165
|
+
|
|
166
|
+
import type { RegistryEntry, RegistryOf, TypeRegistry } from 'mnemonica';
|
|
167
|
+
export type { TypeRegistry };
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
// .tactica/types.ts
|
|
172
|
+
import type { ProtoFlat, LookedUpConstructor, LookedUpInstance } from 'mnemonica';
|
|
173
|
+
import type { TypeRegistry } from './registry';
|
|
174
|
+
|
|
175
|
+
export type User = LookedUpInstance<TypeRegistry, 'User'>; // derived from App
|
|
176
|
+
export type User_Admin = LookedUpInstance<TypeRegistry, 'User.Admin'>;
|
|
177
|
+
|
|
178
|
+
export type Gadget = { // extracted
|
|
179
|
+
g: string;
|
|
180
|
+
Part: LookedUpConstructor<TypeRegistry, 'Gadget.Part', true>;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type Gadget_Part = ProtoFlat<Gadget, {
|
|
184
|
+
size: number;
|
|
185
|
+
Part?: undefined;
|
|
186
|
+
}>;
|
|
179
187
|
```
|
|
180
188
|
|
|
189
|
+
Types held by an exported builder value (`App` above) are **derived**: the registry extends the value's own `RegistryOf<…>` and the aliases read the instances back, so tactica's type and the builder's type are the same type. Everything else is **extracted** from source, written in core's own vocabulary (`RegistryEntry`, `LookedUpConstructor`), so extracted and derived instances and constructors are assignable to each other in both directions. See [Derived and extracted types](#derived-and-extracted-types).
|
|
190
|
+
|
|
191
|
+
A child's own name and its siblings' names are emitted as optional `undefined` (`Part?: undefined`) on the child's instance type: at runtime `part.Part` is `undefined` and `new part.Part()` throws, and the marker keeps the parent's constructor field from being inherited through `ProtoFlat`.
|
|
192
|
+
|
|
181
193
|
**Recommended for new projects.** Explicit imports, better tree-shaking, no global namespace pollution.
|
|
182
194
|
|
|
183
|
-
**Async constructors.** When the construct handler is an `async function` (or async arrow), `new` resolves to a Promise at runtime
|
|
195
|
+
**Async constructors.** When the construct handler is an `async function` (or async arrow), `new` resolves to a Promise at runtime. An extracted async type's registry entry is `AsyncRegistryEntry<X, 'Path', Parent, [args]>` (core's async counterpart of `RegistryEntry`), so `new` on it, and on the subtype field of its parent, is typed `Promise<X>`, and `await` yields `X`, with its own subtypes. Derived async types get the same typing from core's builder. A class constructor that returns a Promise cannot be written in typed code (TypeScript rejects it with TS2409), so async classes stay typed as the plain instance.
|
|
184
196
|
|
|
185
197
|
**Avoid arrow handlers — use the ESLint plugin.** Construct handlers must be regular functions or classes so the instance can be substituted as `this`. Core rejects sync arrows at `define()` time, but async arrows cannot be detected at runtime and never receive the instance — the failure surfaces far from the cause. The early guard is [`eslint-plugin-no-arrow-this`](https://www.npmjs.com/package/eslint-plugin-no-arrow-this), which flags `this` inside arrow functions before the code runs:
|
|
186
198
|
|
|
@@ -198,16 +210,6 @@ Recommended in every mnemonica project.
|
|
|
198
210
|
|
|
199
211
|
**Instance-type inheritance.** Nested instance types flatten the WHOLE ancestor chain: each nested type is `ProtoFlat<ParentInstance, Self>` (mnemonica's `ProtoFlat` keeps every parent key except the ones `Self` overrides), and the parent instance type is itself a `ProtoFlat` over its own parent — so the ROOT's own constructor-arg fields (`serial` in `WidgetRoot`) are directly present on every descendant instance type, at any depth, alongside intermediate ancestors' fields. If a root field appears missing on a nested type, the usual cause is a stale `.tactica` — regenerate; the composition itself is transitive by construction.
|
|
200
212
|
|
|
201
|
-
### Legacy global-augmentation mode (`--module-augmentation`)
|
|
202
|
-
|
|
203
|
-
Emits one `.tactica/index.d.ts` whose contents live inside `declare global { … }`. Reference it from your code with:
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
/// <reference types="./.tactica/index" />
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
or by adding `./.tactica` to `compilerOptions.typeRoots`. Kept for backwards compatibility with older tactica integrations.
|
|
210
|
-
|
|
211
213
|
## Type-Safe Lookup with `lookup()`
|
|
212
214
|
|
|
213
215
|
Mnemonica core exposes `lookup<K>()` against an empty `TypeRegistry` interface. Tactica's `registry.ts` augments that interface with all of your discovered types:
|
|
@@ -216,8 +218,8 @@ Mnemonica core exposes `lookup<K>()` against an empty `TypeRegistry` interface.
|
|
|
216
218
|
// .tactica/registry.ts (generated)
|
|
217
219
|
declare module 'mnemonica' {
|
|
218
220
|
interface TypeRegistry {
|
|
219
|
-
'UserType':
|
|
220
|
-
'UserType.AdminType':
|
|
221
|
+
'UserType': RegistryEntry<UserType, 'UserType', object, [data: { name: string; email: string }]>;
|
|
222
|
+
'UserType.AdminType': RegistryEntry<UserType_AdminType, 'UserType.AdminType', UserType, [data: { role: string }]>;
|
|
221
223
|
}
|
|
222
224
|
}
|
|
223
225
|
```
|
|
@@ -237,6 +239,35 @@ const admin = new user.AdminType({ role: 'admin' });
|
|
|
237
239
|
|
|
238
240
|
No `as unknown as` casts required. See [`docs/lookup-pattern.md`](docs/lookup-pattern.md) for the consumer-side guide.
|
|
239
241
|
|
|
242
|
+
## Derived and extracted types
|
|
243
|
+
|
|
244
|
+
A project may use both of mnemonica's typing paths: builder values and tactica. For the same runtime type both give TypeScript the same type, on TypeScript 6 and 7. Tactica takes every type a builder value already carries and extracts only what the builder cannot type.
|
|
245
|
+
|
|
246
|
+
**Derived** (the type comes from the value itself):
|
|
247
|
+
|
|
248
|
+
- builder chains held in an exported value on the default collection: `export const App = mnemonica.define(…).define(…)`, and `export const Sessions = App.lookup('User').define('Session', …)`. Every exported value is included, so a subtype defined in a later statement is in the registry; `registry.ts` extends the intersection of their `RegistryOf<typeof X>`;
|
|
249
|
+
- exported free `define` results: `export const Widget = define('Widget', …)` enters as `typeof Widget`.
|
|
250
|
+
|
|
251
|
+
Aliases in `types.ts` read derived instances back with `LookedUpInstance<TypeRegistry, 'Path'>`.
|
|
252
|
+
|
|
253
|
+
**Extracted** (tactica reads the source):
|
|
254
|
+
|
|
255
|
+
- inline or unexported `define`, `@decorate` (and typeomatica's `@Strict` beside it), topologica directory types, Option B collections;
|
|
256
|
+
- a handler that uses a type from the generated `.tactica` anywhere in its signature (`this`, a parameter, the return type), and every exported value whose chain passes through such a handler. Deriving those would be circular: the value's type would depend on the registry it is part of.
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
// extracted: the handler takes a generated type, so typeof Notes depends on TypeRegistry
|
|
260
|
+
export const Notes = Sessions.lookup('User').define('Note', function (this: { text: string }, author: User) {
|
|
261
|
+
this.text = author.name;
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Here tactica extracts `User.Note`, while code holding `Notes` keeps the builder's own type for it; the two cross both ways.
|
|
266
|
+
|
|
267
|
+
Custom collections without Option B are never in the global `TypeRegistry`: the free `lookup()` resolves only the default collection at runtime.
|
|
268
|
+
|
|
269
|
+
Hierarchy, definitions, usages and the creation graph come from tactica for every type, derived or extracted.
|
|
270
|
+
|
|
240
271
|
## What Gets Analyzed
|
|
241
272
|
|
|
242
273
|
### `define()` calls
|
|
@@ -313,6 +344,25 @@ const AdminType = define(UserType, 'AdminType', function (this: { role: string }
|
|
|
313
344
|
const AdminCtor = lookup(App, 'UserType.AdminType');
|
|
314
345
|
```
|
|
315
346
|
|
|
347
|
+
### Looped defines with computed names
|
|
348
|
+
|
|
349
|
+
A define whose name ranges over a const object's keys is recognized when the set of names is closed: a helper parameter typed `keyof typeof Data` (directly or through a local alias), or a `for…of Object.keys(Data)` variable used as the name.
|
|
350
|
+
|
|
351
|
+
```ts
|
|
352
|
+
const Data = { Prepare: { step: 1 }, Produce: { step: 2 } };
|
|
353
|
+
type PhaseName = keyof typeof Data;
|
|
354
|
+
|
|
355
|
+
const definePhase = (name: PhaseName) => {
|
|
356
|
+
const result = Run.define(name, holder<typeof Data[typeof name]>());
|
|
357
|
+
return result;
|
|
358
|
+
};
|
|
359
|
+
for (const key of Object.keys(Data)) {
|
|
360
|
+
if (isPhaseName(key)) Phase.set(key, definePhase(key));
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
`Run.Prepare` and `Run.Produce` enter `hierarchy.json` and `definitions.json` under `Run`, in declaration order, sharing the define site's location. They get no `registry.ts` or `types.ts` entries in any collection: their TypeScript types are the builder value's (`Phase.get(…)`). A name that is not a closed const-object key set (a plain `string`, a computed key, an unresolved alias) is still reported with "Could not extract type name" and skipped.
|
|
365
|
+
|
|
316
366
|
### Custom collections via `createTypesCollection()`
|
|
317
367
|
|
|
318
368
|
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.
|
|
@@ -426,24 +476,29 @@ const sub = new gadget.EnhancedGadget();
|
|
|
426
476
|
### `Object.assign(this, data)` pattern
|
|
427
477
|
|
|
428
478
|
```ts
|
|
429
|
-
|
|
430
|
-
|
|
479
|
+
// a handler without a `this` annotation
|
|
480
|
+
const UserType = define('UserType', function (data: { name: string }) {
|
|
481
|
+
Object.assign(this, data); // properties inferred from `data`'s type
|
|
431
482
|
});
|
|
432
483
|
```
|
|
433
484
|
|
|
485
|
+
In a handler with a `this` annotation the annotation is the type (see [Property Type Inference](#property-type-inference)); `Object.assign` then adds nothing beyond it.
|
|
486
|
+
|
|
434
487
|
### `TypeConstructor` casting (from mnemonica)
|
|
435
488
|
|
|
436
489
|
```ts
|
|
437
490
|
import { define } from 'mnemonica';
|
|
438
491
|
import type { TypeConstructor } from 'mnemonica';
|
|
439
492
|
|
|
440
|
-
const MyFn = function (this:
|
|
493
|
+
const MyFn = function (this: { field: number }) {
|
|
441
494
|
this.field = 123;
|
|
442
495
|
} as TypeConstructor<{ field: number }>;
|
|
443
496
|
|
|
444
|
-
const MyFnType = define('MyFnType', MyFn);
|
|
497
|
+
const MyFnType = define('MyFnType', MyFn); // MyFnType: { field: number }
|
|
445
498
|
```
|
|
446
499
|
|
|
500
|
+
The analyzer follows `MyFn` to its function expression and extracts it exactly like an inline handler. The `TypeConstructor` type argument is not used as a substitute for the function body; the annotated-`this` rule applies, so `this: { field: number }` gives `{ field: number }` (and `this: any` would give `any`). Const-held function and class expressions, function and class declarations, and imported handlers are followed through their identifiers, including through `lazy()`, `Type.define()`, and chained `define()` calls. An identifier whose declaration cannot be resolved keeps the previous empty extraction.
|
|
501
|
+
|
|
447
502
|
(In older mnemonica documentation this helper was called `ConstructorFunction`; the analyzer still recognizes that name in source code for back-compat.)
|
|
448
503
|
|
|
449
504
|
### Typeomatica integration
|
|
@@ -490,7 +545,21 @@ npx tactica --topologica ./src/ai-types,./custom/topologica
|
|
|
490
545
|
|
|
491
546
|
## Property Type Inference
|
|
492
547
|
|
|
493
|
-
|
|
548
|
+
**An annotated `this` is the type.** A handler is used as a constructor, so its `this` annotation states the instance, the way a class is typed by its declared fields. Assignments beyond the annotation add nothing:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
interface PackData { data?: Record<string, unknown> }
|
|
552
|
+
|
|
553
|
+
define('Pack', function (this: PackData, files: PackFiles, meta: PackMeta) {
|
|
554
|
+
this.header = files.header; // not in PackData: not typed
|
|
555
|
+
this.data = files.extra;
|
|
556
|
+
});
|
|
557
|
+
// Pack is PackData: { data?: Record<string, unknown> }
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
Declare the fields in the annotation to have them typed. `this: any` is respected the same way: the instance type is `any`. Two cases keep reading the body: a handler without a `this` annotation, and a handler whose `this` is a type from the generated `.tactica` (the [call-site ergonomics](#define-calls) pattern), since that annotation is tactica's own output.
|
|
561
|
+
|
|
562
|
+
For handlers without a `this` annotation, the analyzer infers property types from constructor bodies and class members. Supported expression patterns:
|
|
494
563
|
|
|
495
564
|
| Pattern | Inferred type |
|
|
496
565
|
|---|---|
|
|
@@ -709,7 +778,6 @@ class TypesGenerator {
|
|
|
709
778
|
|
|
710
779
|
generateTypesFile(): GeneratedTypes; // default mode → types.ts
|
|
711
780
|
generateTypeRegistry(): GeneratedTypes; // default mode → registry.ts
|
|
712
|
-
generateGlobalAugmentation(): GeneratedTypes; // legacy mode → index.d.ts
|
|
713
781
|
generateSingleType(node: TypeNode): string;
|
|
714
782
|
}
|
|
715
783
|
```
|
|
@@ -776,7 +844,6 @@ class TypesWriter {
|
|
|
776
844
|
constructor(outputDir?: string);
|
|
777
845
|
|
|
778
846
|
writeTypesFile(generated: GeneratedTypes): string; // → outputDir/types.ts
|
|
779
|
-
writeGlobalAugmentation(generated: GeneratedTypes): string; // → outputDir/index.d.ts
|
|
780
847
|
writeTo(filename: string, content: string): string; // → outputDir/<filename>
|
|
781
848
|
|
|
782
849
|
writeDefinitionsFile(map: Map<string, DefinitionInfo>, graph?: TypeGraph): string; // → outputDir/definitions.json (graph adds fields/args)
|
|
@@ -936,7 +1003,7 @@ Deliberate approximations (name-based, no type checker): namespace imports count
|
|
|
936
1003
|
1. **Parse** — load `tsconfig.json`, build a `ts.Program`, walk each source file's AST.
|
|
937
1004
|
2. **Detect** — find `define()` and `@decorate()` calls, plus `lookup` lookups, `new` expressions, and EDS / flow patterns.
|
|
938
1005
|
3. **Graph** — build a Trie of types in `TypeGraphImpl`, with parent links via the chain of `.define()` calls and `@decorate(Parent)` references.
|
|
939
|
-
4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts
|
|
1006
|
+
4. **Generate** — emit `types.ts`, `registry.ts`, `index.ts`, plus `definitions.json`, `usages.json` (with `holderScopeId`), `flow.json`, `instrumentation.json`, `control.json`, `modules.json`, `scopes.json`, `hierarchy.json`/`hierarchy.txt`, `collections.json`, and optionally `eds.json`.
|
|
940
1007
|
5. **Write** — files land in the output directory (default `.tactica/`).
|
|
941
1008
|
|
|
942
1009
|
```
|
|
@@ -976,7 +1043,7 @@ Multi-level property access in constructors falls back to `unknown`:
|
|
|
976
1043
|
|
|
977
1044
|
```ts
|
|
978
1045
|
// ⚠️ this.name's inferred type is unknown
|
|
979
|
-
const UserType = define('UserType', function (
|
|
1046
|
+
const UserType = define('UserType', function (data: { profile: { name: string } }) {
|
|
980
1047
|
this.name = data.profile.name;
|
|
981
1048
|
});
|
|
982
1049
|
```
|
|
@@ -1011,7 +1078,7 @@ Custom collection types are isolated in the analyzer by their collection identit
|
|
|
1011
1078
|
|
|
1012
1079
|
## Troubleshooting
|
|
1013
1080
|
|
|
1014
|
-
**Generated types are missing properties** —
|
|
1081
|
+
**Generated types are missing properties** — with a `this:` annotation, the annotation is the type: declare the missing fields there. Without one, make sure the data parameter has an inline / aliased type (`{ id: string }` or `Foo` where `type Foo = { id: string }`); property inference walks one level of indirection. For a derived type (an exported builder value), the fields are whatever the builder's own type carries.
|
|
1015
1082
|
|
|
1016
1083
|
**`new user.AdminType()` shows red squiggles** — confirm `.tactica/types.ts` is in your `tsconfig.json` `include`. If you use `lookup`, also import `.tactica/registry.ts` somewhere.
|
|
1017
1084
|
|
package/SKILL.md
CHANGED
|
@@ -50,27 +50,42 @@ augmented interface instead of the global `TypeRegistry`.
|
|
|
50
50
|
|
|
51
51
|
## What it types
|
|
52
52
|
|
|
53
|
+
- **Derived types:** an exported builder value on the default collection
|
|
54
|
+
(`export const App = mnemonica.define(…).define(…)`,
|
|
55
|
+
`export const Sessions = App.lookup('User').define(…)`) or an exported
|
|
56
|
+
free `define` result is not re-extracted: `registry.ts` extends its
|
|
57
|
+
`RegistryOf<typeof X>` (or uses `typeof X`), and `types.ts` reads the
|
|
58
|
+
instances back with `LookedUpInstance<TypeRegistry, 'Path'>`. The
|
|
59
|
+
builder and tactica then give the same type for the same runtime type.
|
|
60
|
+
- **Extracted types:** everything else (inline or unexported `define`,
|
|
61
|
+
`@decorate`, handlers that use a generated `.tactica` type, topologica,
|
|
62
|
+
Option B) is written out in core's vocabulary: registry entries are
|
|
63
|
+
`RegistryEntry<X, 'Path', Parent, [args]>`, subtype fields
|
|
64
|
+
`LookedUpConstructor<TypeRegistry, 'Path', true>`, so extracted and
|
|
65
|
+
derived instances are assignable both ways.
|
|
53
66
|
- One `export type` per discovered type, named by dotted path with `_`
|
|
54
|
-
separators; nested types are `ProtoFlat<Parent, Self>`, so
|
|
55
|
-
fields are present on descendants by construction.
|
|
56
|
-
-
|
|
57
|
-
`
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
67
|
+
separators; nested extracted types are `ProtoFlat<Parent, Self>`, so
|
|
68
|
+
ancestor fields are present on descendants by construction.
|
|
69
|
+
- **An annotated `this` is the type:** `function (this: Shape, …)` gives
|
|
70
|
+
`Shape`, and assignments beyond it add nothing (`this: any` gives
|
|
71
|
+
`any`). Only a handler without a `this` annotation is typed from its
|
|
72
|
+
body.
|
|
73
|
+
- Handlers held in a const (`const h = function …; define('X', h)`) are
|
|
74
|
+
followed like inline ones.
|
|
75
|
+
- Looped defines over a const object's keys enter hierarchy and
|
|
76
|
+
definitions only; their types come from the builder value.
|
|
61
77
|
|
|
62
78
|
## Async constructors
|
|
63
79
|
|
|
64
|
-
When a construct handler is an `async function` (or async arrow),
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
80
|
+
When a construct handler is an `async function` (or async arrow), `new`
|
|
81
|
+
resolves to a Promise at runtime. Extracted async types are registered as
|
|
82
|
+
`AsyncRegistryEntry<X, …>` (mnemonica ≥ 1.4.7), so construction is typed
|
|
83
|
+
`Promise<X>` and `await` yields `X` with its own subtypes; derived async
|
|
84
|
+
types get the same typing from mnemonica's builder.
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
userland when `Promise<X>` is needed there.
|
|
86
|
+
A class constructor returning a Promise cannot be written in typed code
|
|
87
|
+
(TypeScript rejects it, TS2409), so async **classes** stay typed as the
|
|
88
|
+
plain instance.
|
|
74
89
|
|
|
75
90
|
## Avoid arrow handlers — use the ESLint plugin
|
|
76
91
|
|
|
@@ -107,16 +122,16 @@ written by contributors who have not internalized the rule.
|
|
|
107
122
|
import { mnemonica } from 'mnemonica';
|
|
108
123
|
|
|
109
124
|
export const App = mnemonica
|
|
110
|
-
.define('Widget', function (this:
|
|
125
|
+
.define('Widget', function (this: { id: string }, data: { id: string }) {
|
|
111
126
|
this.id = data.id;
|
|
112
127
|
})
|
|
113
|
-
.define('Gadget', async function (this:
|
|
128
|
+
.define('Gadget', async function (this: { serial: number }, data: { serial: number }) {
|
|
114
129
|
this.serial = await register(data.serial);
|
|
115
130
|
return this;
|
|
116
131
|
});
|
|
117
132
|
```
|
|
118
133
|
|
|
119
|
-
After `npx tactica`, `lookup('
|
|
120
|
-
`new (
|
|
121
|
-
`await` is
|
|
122
|
-
`undefined` at runtime.
|
|
134
|
+
After `npx tactica`, `App` is derived, and the free `lookup('Widget')` is
|
|
135
|
+
typed exactly as `App.lookup('Widget')`. `new widget.Gadget({ serial: 1 })`
|
|
136
|
+
is a `Promise` — a forgotten `await` is a type error at the call site
|
|
137
|
+
instead of a silent `undefined` at runtime.
|
package/lib/analyzer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo, InstrumentationPoint, ResolutionError, CollectionManifestEntry } from './types';
|
|
2
|
+
import { AnalyzeResult, DefinitionInfo, UsageInfo, EDSInfo, FlowInfo, InstrumentationPoint, ResolutionError, CollectionManifestEntry, BuilderValue } from './types';
|
|
3
3
|
import { TypeGraphImpl } from './graph';
|
|
4
4
|
import { TacticaPlugin } from './plugins';
|
|
5
5
|
/**
|
|
@@ -18,9 +18,13 @@ export declare class MnemonicaAnalyzer {
|
|
|
18
18
|
private flowUsages;
|
|
19
19
|
private edsScopeByNode;
|
|
20
20
|
private functionBindings;
|
|
21
|
+
private constructorBindings;
|
|
21
22
|
private nestedWrapVia;
|
|
22
23
|
private wrapEntryByNode;
|
|
23
24
|
private variableToTypeMap;
|
|
25
|
+
private builderCandidates;
|
|
26
|
+
private excludedTypePaths;
|
|
27
|
+
private generatedTypeImports;
|
|
24
28
|
private moduleObjectVariables;
|
|
25
29
|
private mnemonicaNamedImports;
|
|
26
30
|
private createTypesCollectionVariables;
|
|
@@ -69,6 +73,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
69
73
|
* Get the type graph
|
|
70
74
|
*/
|
|
71
75
|
getGraph(): TypeGraphImpl;
|
|
76
|
+
getBuilderValues(): BuilderValue[];
|
|
72
77
|
/**
|
|
73
78
|
* Get collected definitions
|
|
74
79
|
*/
|
|
@@ -119,6 +124,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
119
124
|
* Visit a node in the AST
|
|
120
125
|
*/
|
|
121
126
|
private visitNode;
|
|
127
|
+
private trackConstructorBinding;
|
|
122
128
|
/**
|
|
123
129
|
* Track imports from 'mnemonica' so aliases of the module object and
|
|
124
130
|
* createTypesCollection are recognized without relying on the type checker.
|
|
@@ -470,6 +476,7 @@ export declare class MnemonicaAnalyzer {
|
|
|
470
476
|
* Process a define() call
|
|
471
477
|
*/
|
|
472
478
|
private processDefineCall;
|
|
479
|
+
private processDefineContext;
|
|
473
480
|
/**
|
|
474
481
|
* Process a lazy() call
|
|
475
482
|
*/
|
|
@@ -510,6 +517,11 @@ export declare class MnemonicaAnalyzer {
|
|
|
510
517
|
* Extract config options from lazy() call
|
|
511
518
|
*/
|
|
512
519
|
private extractLazyConfig;
|
|
520
|
+
private hasGeneratedTypeReference;
|
|
521
|
+
private hasSelfTypeAnnotation;
|
|
522
|
+
private recordBuilderCandidate;
|
|
523
|
+
private markBuilderExport;
|
|
524
|
+
private trackBuilderExportAssignment;
|
|
513
525
|
/**
|
|
514
526
|
* Track variable assignments that capture define() results
|
|
515
527
|
* e.g., const User = define('UserEntity', ...) maps "User" -> "UserEntity"
|
|
@@ -612,6 +624,17 @@ export declare class MnemonicaAnalyzer {
|
|
|
612
624
|
* define(() => class TypeName {})
|
|
613
625
|
*/
|
|
614
626
|
private extractTypeName;
|
|
627
|
+
/**
|
|
628
|
+
* Resolve a computed define name only when it ranges over a closed const
|
|
629
|
+
* object key set. Arbitrary strings and unresolved unions stay on the
|
|
630
|
+
* existing warning-and-skip path.
|
|
631
|
+
*/
|
|
632
|
+
private extractComputedTypeNames;
|
|
633
|
+
private resolveLoopKeyNames;
|
|
634
|
+
private resolveObjectKeysCall;
|
|
635
|
+
private resolveParameterKeyNames;
|
|
636
|
+
private resolveKeyofType;
|
|
637
|
+
private resolveConstObjectKeys;
|
|
615
638
|
/**
|
|
616
639
|
* Extract the full define() call context: type name, parent type, and collection.
|
|
617
640
|
* Handles direct calls, property-access calls, chained calls, and the
|
|
@@ -670,10 +693,14 @@ export declare class MnemonicaAnalyzer {
|
|
|
670
693
|
* Get property chain from nested access
|
|
671
694
|
*/
|
|
672
695
|
private getPropertyChain;
|
|
696
|
+
private findConstructorInModule;
|
|
697
|
+
private resolveHeldConstructor;
|
|
698
|
+
private unwrapConstructorAssertions;
|
|
699
|
+
private resolveConstructorExpression;
|
|
673
700
|
/**
|
|
674
701
|
* Determine the constructor expression for either a define() or lazy() call.
|
|
675
702
|
* For define() this is the construct handler; for lazy() it is the value
|
|
676
|
-
* returned by the lazy getter.
|
|
703
|
+
* returned by the lazy getter. Identifiers resolve to their declarations.
|
|
677
704
|
*/
|
|
678
705
|
private extractConstructorExpression;
|
|
679
706
|
/**
|
|
@@ -720,6 +747,8 @@ export declare class MnemonicaAnalyzer {
|
|
|
720
747
|
* Infer method type from method declaration
|
|
721
748
|
*/
|
|
722
749
|
private inferMethodType;
|
|
750
|
+
private hasGeneratedThisAnnotation;
|
|
751
|
+
private hasAnyThisAnnotation;
|
|
723
752
|
/**
|
|
724
753
|
* Extract properties from `this` parameter type annotation
|
|
725
754
|
* Handles patterns like: function(this: SomeType, data: SomeType) { }
|