@llui/vite-plugin 0.0.31 → 0.0.33

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.
@@ -0,0 +1,109 @@
1
+ import ts from 'typescript';
2
+ import { type MessageAnnotations } from './msg-annotations.js';
3
+ import { type MsgSchema } from './msg-schema.js';
4
+ /**
5
+ * Cross-file type resolver.
6
+ *
7
+ * The schema/annotation extractors (`extractMsgAnnotations`,
8
+ * `extractMsgSchema`, `extractStateSchema`, `extractEffectSchema`) only
9
+ * see the source string for the file currently being transformed. When
10
+ * a developer keeps the `Msg` (or `State` / `Effect`) union in a
11
+ * separate file and imports it where `component()` is called, those
12
+ * extractors silently return `null` — the plugin emits no annotations,
13
+ * runtime LAP validation is disabled, and Claude can dispatch arbitrary
14
+ * `type` strings that fall through to `assertNever`.
15
+ *
16
+ * This module follows imports and re-exports to find the source file
17
+ * that declares the requested type alias, returning that file's source
18
+ * string + the local name of the alias there. Extractors then run
19
+ * against that source and produce the same output they would have for
20
+ * a co-located declaration.
21
+ *
22
+ * Limitations:
23
+ * - Composition (`type Msg = ImportedA | { type: 'b' }`): only the
24
+ * locally-declared variants are extracted; the imported half isn't
25
+ * walked recursively into. The lint rule `agent-msg-resolvable`
26
+ * catches this case at lint time.
27
+ * - Namespace imports (`import * as ns from './msg'`) and `export *`:
28
+ * not followed. Same lint coverage.
29
+ * - Generic types: not parameterized resolution; the type argument
30
+ * must resolve to a concrete type alias.
31
+ */
32
+ export interface ResolveContext {
33
+ /**
34
+ * Resolve a module specifier (e.g. `'./msg'`, `'@scope/pkg'`) against
35
+ * the importing file's path. Returns the absolute filesystem path of
36
+ * the resolved module, or `null` if it cannot be resolved (the type
37
+ * stays unresolved and the extractor falls back to local-only mode).
38
+ */
39
+ resolveModule: (spec: string, importerPath: string) => Promise<string | null>;
40
+ /**
41
+ * Read the source contents of an absolute module path. The contents
42
+ * are parsed by TypeScript so they should be valid TS/TSX. The plugin
43
+ *'s vite hook plumbs `fs/promises.readFile` here; tests provide an
44
+ * in-memory map.
45
+ */
46
+ readSource: (absolutePath: string) => Promise<string>;
47
+ }
48
+ export interface ResolvedTypeSource {
49
+ /** The full source string of the file declaring the type alias. */
50
+ source: string;
51
+ /** The local name of the alias *in that file* (after rename chains). */
52
+ localName: string;
53
+ /** Absolute path of the file declaring the alias (debug aid). */
54
+ filePath: string;
55
+ }
56
+ /**
57
+ * Walk imports + re-exports to find where a type alias is actually
58
+ * declared. Returns the source string and local name of the alias in
59
+ * its declaring file. Returns `null` if the chain leads to an unresolved
60
+ * module, a re-export through `export *`, a namespace import, or a
61
+ * dead-end (alias not declared anywhere we can see).
62
+ */
63
+ export declare function findTypeSource(typeName: string, source: string, filePath: string, ctx: ResolveContext, visited?: Set<string>): Promise<ResolvedTypeSource | null>;
64
+ /**
65
+ * Annotation extractor that walks composed Msg unions across files.
66
+ *
67
+ * Given a Msg type that may be a union of inline `{ type: 'literal' }`
68
+ * objects AND TypeReferences (e.g.
69
+ * `type Msg = ImportedFoo | { type: 'extra' }`), recursively follow
70
+ * each TypeReference via `findTypeSource` and merge its variants into
71
+ * the returned map.
72
+ *
73
+ * Composition + cross-file is the union of two failure modes the
74
+ * file-local sync extractor silently mishandles. This function
75
+ * produces the same map the runtime expects regardless of how the
76
+ * developer organized the type declarations.
77
+ *
78
+ * Conflict policy: if two composed branches contribute the same
79
+ * discriminant string (e.g. both halves declare `{ type: 'inc' }`),
80
+ * the first one walked wins. The lint rule `agent-msg-resolvable`
81
+ * fires before this point on most pathological cases; ESLint's
82
+ * type-checker would flag the duplicate independently.
83
+ */
84
+ export declare function extractMsgAnnotationsCrossFile(source: string, typeName: string, filePath: string, ctx: ResolveContext): Promise<Record<string, MessageAnnotations> | null>;
85
+ /**
86
+ * Cross-file companion to `extractMsgSchema` / `extractEffectSchema`.
87
+ *
88
+ * Discriminated-union schema extractor that follows composed
89
+ * TypeReferences through the resolver. Same recursion shape as
90
+ * `extractMsgAnnotationsCrossFile`, just collecting field shapes
91
+ * instead of JSDoc annotations.
92
+ */
93
+ export declare function extractDiscriminatedUnionSchemaCrossFile(source: string, typeName: string, filePath: string, ctx: ResolveContext): Promise<MsgSchema | null>;
94
+ /**
95
+ * Inspect the type arguments of a `component<...>()` call and return
96
+ * the textual identifier for each known position. Returns `null` for
97
+ * positions whose type argument isn't a plain identifier (e.g.
98
+ * inline literal types, generic instantiations, namespace-qualified
99
+ * names). Identifiers are what the resolver can chase; everything else
100
+ * we leave to the local extractor's existing behavior.
101
+ *
102
+ * Order: `[State, Msg, Effect]` matching `component<State, Msg, Effect>`.
103
+ */
104
+ export declare function readComponentTypeArgNames(call: ts.CallExpression): {
105
+ state: string | null;
106
+ msg: string | null;
107
+ effect: string | null;
108
+ };
109
+ //# sourceMappingURL=cross-file-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-file-resolver.d.ts","sourceRoot":"","sources":["../src/cross-file-resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAA;AAC3B,OAAO,EACL,KAAK,kBAAkB,EAExB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EACL,KAAK,SAAS,EAIf,MAAM,iBAAiB,CAAA;AAExB;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAA;IAC7E;;;;;OAKG;IACH,UAAU,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAA;CACtD;AAED,MAAM,WAAW,kBAAkB;IACjC,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAA;IACd,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAA;IACjB,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,cAAc,EACnB,OAAO,GAAE,GAAG,CAAC,MAAM,CAAa,GAC/B,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAwGpC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,CAAC,CAKpD;AA6KD;;;;;;;GAOG;AACH,wBAAsB,wCAAwC,CAC5D,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAK3B;AA2JD;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,GAAG;IAClE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;IACpB,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IAClB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;CACtB,CASA"}
@@ -0,0 +1,457 @@
1
+ import ts from 'typescript';
2
+ import {} from './msg-annotations.js';
3
+ import { buildFieldDescriptor, } from './msg-schema.js';
4
+ /**
5
+ * Walk imports + re-exports to find where a type alias is actually
6
+ * declared. Returns the source string and local name of the alias in
7
+ * its declaring file. Returns `null` if the chain leads to an unresolved
8
+ * module, a re-export through `export *`, a namespace import, or a
9
+ * dead-end (alias not declared anywhere we can see).
10
+ */
11
+ export async function findTypeSource(typeName, source, filePath, ctx, visited = new Set()) {
12
+ // Cycle prevention — re-export A → A is a tight loop that some
13
+ // pathological re-export chains can produce. Bail rather than
14
+ // infinitely recurse.
15
+ if (visited.has(`${filePath}::${typeName}`))
16
+ return null;
17
+ visited.add(`${filePath}::${typeName}`);
18
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true);
19
+ // 1. Local declaration wins. `type X = ...` or `interface X { ... }`
20
+ // (extractors only support type aliases today, but check both so
21
+ // the resolver itself isn't a footgun for future extractors).
22
+ for (const stmt of sf.statements) {
23
+ if (ts.isTypeAliasDeclaration(stmt) && stmt.name.text === typeName) {
24
+ return { source, localName: typeName, filePath };
25
+ }
26
+ if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === typeName) {
27
+ return { source, localName: typeName, filePath };
28
+ }
29
+ }
30
+ // 2. Re-export with name: `export { X } from './y'` or
31
+ // `export { X as Y } from './y'`. Walk to the source module.
32
+ for (const stmt of sf.statements) {
33
+ if (!ts.isExportDeclaration(stmt))
34
+ continue;
35
+ if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause))
36
+ continue;
37
+ if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
38
+ continue;
39
+ for (const spec of stmt.exportClause.elements) {
40
+ const exportedName = spec.name.text;
41
+ if (exportedName !== typeName)
42
+ continue;
43
+ // The name in the source module is `propertyName` if present
44
+ // (e.g. `export { Msg as M } from './msg'` exports as M but the
45
+ // source module has it as Msg).
46
+ const sourceName = spec.propertyName?.text ?? spec.name.text;
47
+ const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath);
48
+ if (!resolved)
49
+ return null;
50
+ const subSource = await ctx.readSource(resolved);
51
+ return findTypeSource(sourceName, subSource, resolved, ctx, visited);
52
+ }
53
+ }
54
+ // 3. Local re-binding: `export { X } from elsewhere` shorthand was
55
+ // handled above. A separate case is `import { X } from ... ; export
56
+ // { X }` — the import already declares X locally, so step 5 picks
57
+ // it up.
58
+ // 4. Star re-exports: `export * from './y'`. The barrel re-exports
59
+ // every named member of `./y` under the same name. Walk each
60
+ // barrel target and return the first hit. Order: textual order
61
+ // in the source file (matches TypeScript's behaviour for
62
+ // multi-barrel name collisions, where the first declared wins).
63
+ //
64
+ // Multiple `export *` declarations are common in monorepo barrel
65
+ // files (`export * from './msg'; export * from './effects'`).
66
+ // Without this step, the resolver returns `null` and the plugin
67
+ // silently emits empty annotations for any consumer that points
68
+ // at a barrel.
69
+ for (const stmt of sf.statements) {
70
+ if (!ts.isExportDeclaration(stmt))
71
+ continue;
72
+ // `export * from './y'` has no exportClause; `export {} from './y'`
73
+ // is a different beast (re-exports nothing). Skip the latter.
74
+ if (stmt.exportClause)
75
+ continue;
76
+ if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
77
+ continue;
78
+ const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath);
79
+ if (!resolved)
80
+ continue;
81
+ let subSource;
82
+ try {
83
+ subSource = await ctx.readSource(resolved);
84
+ }
85
+ catch {
86
+ // Module path resolved but the file isn't readable (deleted,
87
+ // dynamic-only, etc.). Continue to the next barrel.
88
+ continue;
89
+ }
90
+ const found = await findTypeSource(typeName, subSource, resolved, ctx, visited);
91
+ if (found)
92
+ return found;
93
+ }
94
+ // 5. Imports: `import { X } from './y'` or `import { X as Y } from './y'`.
95
+ // Walk to the source module using the original (imported) name.
96
+ for (const stmt of sf.statements) {
97
+ if (!ts.isImportDeclaration(stmt))
98
+ continue;
99
+ if (!stmt.importClause)
100
+ continue;
101
+ if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier))
102
+ continue;
103
+ const bindings = stmt.importClause.namedBindings;
104
+ if (!bindings || !ts.isNamedImports(bindings))
105
+ continue;
106
+ for (const elem of bindings.elements) {
107
+ const localName = elem.name.text;
108
+ if (localName !== typeName)
109
+ continue;
110
+ // The remote name is `propertyName` when there's a rename, else
111
+ // the local name itself.
112
+ const remoteName = elem.propertyName?.text ?? elem.name.text;
113
+ const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath);
114
+ if (!resolved)
115
+ return null;
116
+ const subSource = await ctx.readSource(resolved);
117
+ return findTypeSource(remoteName, subSource, resolved, ctx, visited);
118
+ }
119
+ }
120
+ // Not found in this file and no import/re-export to follow.
121
+ return null;
122
+ }
123
+ /**
124
+ * Annotation extractor that walks composed Msg unions across files.
125
+ *
126
+ * Given a Msg type that may be a union of inline `{ type: 'literal' }`
127
+ * objects AND TypeReferences (e.g.
128
+ * `type Msg = ImportedFoo | { type: 'extra' }`), recursively follow
129
+ * each TypeReference via `findTypeSource` and merge its variants into
130
+ * the returned map.
131
+ *
132
+ * Composition + cross-file is the union of two failure modes the
133
+ * file-local sync extractor silently mishandles. This function
134
+ * produces the same map the runtime expects regardless of how the
135
+ * developer organized the type declarations.
136
+ *
137
+ * Conflict policy: if two composed branches contribute the same
138
+ * discriminant string (e.g. both halves declare `{ type: 'inc' }`),
139
+ * the first one walked wins. The lint rule `agent-msg-resolvable`
140
+ * fires before this point on most pathological cases; ESLint's
141
+ * type-checker would flag the duplicate independently.
142
+ */
143
+ export async function extractMsgAnnotationsCrossFile(source, typeName, filePath, ctx) {
144
+ const out = {};
145
+ const ok = await collectMsgVariants(typeName, source, filePath, ctx, out, new Set());
146
+ if (!ok)
147
+ return null;
148
+ return Object.keys(out).length === 0 ? null : out;
149
+ }
150
+ async function collectMsgVariants(typeName, source, filePath, ctx, out, visitedAliases) {
151
+ const located = await findTypeSource(typeName, source, filePath, ctx, new Set());
152
+ if (!located)
153
+ return false;
154
+ const aliasKey = `${located.filePath}::${located.localName}`;
155
+ if (visitedAliases.has(aliasKey))
156
+ return true;
157
+ visitedAliases.add(aliasKey);
158
+ const sf = ts.createSourceFile(located.filePath, located.source, ts.ScriptTarget.Latest, true);
159
+ const aliases = [];
160
+ sf.forEachChild((n) => {
161
+ if (ts.isTypeAliasDeclaration(n))
162
+ aliases.push(n);
163
+ });
164
+ const alias = aliases.find((a) => a.name.text === located.localName);
165
+ if (!alias)
166
+ return false;
167
+ // Single-variant alias: `type Foo = { type: 'a', ... }`. Treat as a
168
+ // one-element union so a Msg variant can be its own type alias.
169
+ const memberNodes = ts.isUnionTypeNode(alias.type)
170
+ ? [...alias.type.types]
171
+ : [alias.type];
172
+ for (let i = 0; i < memberNodes.length; i++) {
173
+ const member = memberNodes[i];
174
+ if (ts.isTypeLiteralNode(member)) {
175
+ const variant = readDiscriminantLiteral(member);
176
+ if (!variant)
177
+ continue;
178
+ const comment = readLeadingJSDocForMember(located.source, alias, memberNodes, i);
179
+ if (out[variant] === undefined) {
180
+ out[variant] = parseMessageAnnotations(comment);
181
+ }
182
+ continue;
183
+ }
184
+ if (ts.isTypeReferenceNode(member) && ts.isIdentifier(member.typeName)) {
185
+ // Composed: recurse through the resolver.
186
+ await collectMsgVariants(member.typeName.text, located.source, located.filePath, ctx, out, visitedAliases);
187
+ continue;
188
+ }
189
+ // Other shapes (intersections, conditional types, namespace-qualified
190
+ // names) aren't followed. Lint catches this.
191
+ }
192
+ return true;
193
+ }
194
+ function readDiscriminantLiteral(lit) {
195
+ for (const m of lit.members) {
196
+ if (!ts.isPropertySignature(m))
197
+ continue;
198
+ if (!m.name || !ts.isIdentifier(m.name) || m.name.text !== 'type')
199
+ continue;
200
+ if (!m.type || !ts.isLiteralTypeNode(m.type))
201
+ continue;
202
+ const literal = m.type.literal;
203
+ if (ts.isStringLiteral(literal))
204
+ return literal.text;
205
+ }
206
+ return null;
207
+ }
208
+ /**
209
+ * Read leading JSDoc for a union member at index `i` of `members`.
210
+ * The JSDoc lives between the previous element's end and the current
211
+ * element's start (or between the type alias start and the first
212
+ * element for `i === 0`). Mirrors the logic in
213
+ * `extractMsgAnnotations` so the cross-file path produces the same
214
+ * output for the same input.
215
+ */
216
+ function readLeadingJSDocForMember(source, alias, members, i) {
217
+ const prev = members[i - 1];
218
+ const member = members[i];
219
+ // For non-union (single-variant) aliases the union pos is the alias
220
+ // body's pos.
221
+ const unionPos = ts.isUnionTypeNode(alias.type) ? alias.type.pos : alias.type.pos;
222
+ const scanPos = i === 0 || prev === undefined ? unionPos : prev.end;
223
+ const ranges = ts.getLeadingCommentRanges(source, scanPos) ?? [];
224
+ const docs = ranges
225
+ .filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)
226
+ .map((r) => source.slice(r.pos, r.end))
227
+ .filter((txt) => txt.startsWith('/**'));
228
+ // Cut off comments that appear AFTER the member starts (rare but
229
+ // possible with weird formatting).
230
+ const _end = member.pos;
231
+ return docs.join('\n');
232
+ }
233
+ function parseMessageAnnotations(comment) {
234
+ if (!comment)
235
+ return defaultMessageAnnotations();
236
+ const intent = readIntentTag(comment);
237
+ const human = /@humanOnly\b/.test(comment);
238
+ const agent = /@agentOnly\b/.test(comment);
239
+ const dispatchMode = human && !agent ? 'human-only' : agent && !human ? 'agent-only' : 'shared';
240
+ return {
241
+ intent,
242
+ alwaysAffordable: /@alwaysAffordable\b/.test(comment),
243
+ requiresConfirm: /@requiresConfirm\b/.test(comment),
244
+ dispatchMode,
245
+ examples: readExamplesTag(comment),
246
+ warning: readWarningTag(comment),
247
+ emits: readEmitsTag(comment),
248
+ };
249
+ }
250
+ function readEmitsTag(comment) {
251
+ const outer = comment.match(/@emits\s*\(([^)]*)\)/);
252
+ if (!outer || outer[1] === undefined)
253
+ return [];
254
+ const inner = outer[1];
255
+ const seen = new Set();
256
+ const out = [];
257
+ const re = /["“]([^"”]*)["”]/g;
258
+ let m;
259
+ while ((m = re.exec(inner)) !== null) {
260
+ const v = m[1];
261
+ if (v === undefined || seen.has(v))
262
+ continue;
263
+ seen.add(v);
264
+ out.push(v);
265
+ }
266
+ return out;
267
+ }
268
+ function readExamplesTag(comment) {
269
+ const out = [];
270
+ const re = /@example\s*\(\s*["“]([^"”]*)["”]\s*\)/g;
271
+ let m;
272
+ while ((m = re.exec(comment)) !== null) {
273
+ if (m[1] !== undefined)
274
+ out.push(m[1]);
275
+ }
276
+ return out;
277
+ }
278
+ function readWarningTag(comment) {
279
+ const match = comment.match(/@warning\s*\(\s*["“]([^"”]*)["”]\s*\)/);
280
+ return match?.[1] ?? null;
281
+ }
282
+ function defaultMessageAnnotations() {
283
+ return {
284
+ intent: null,
285
+ alwaysAffordable: false,
286
+ requiresConfirm: false,
287
+ dispatchMode: 'shared',
288
+ examples: [],
289
+ warning: null,
290
+ emits: [],
291
+ };
292
+ }
293
+ function readIntentTag(comment) {
294
+ const match = comment.match(/@intent\s*\(\s*["“]([^"”]*)["”]\s*\)/);
295
+ return match?.[1] ?? null;
296
+ }
297
+ /**
298
+ * Cross-file companion to `extractMsgSchema` / `extractEffectSchema`.
299
+ *
300
+ * Discriminated-union schema extractor that follows composed
301
+ * TypeReferences through the resolver. Same recursion shape as
302
+ * `extractMsgAnnotationsCrossFile`, just collecting field shapes
303
+ * instead of JSDoc annotations.
304
+ */
305
+ export async function extractDiscriminatedUnionSchemaCrossFile(source, typeName, filePath, ctx) {
306
+ const variants = {};
307
+ const ok = await collectSchemaVariants(typeName, source, filePath, ctx, variants, new Set());
308
+ if (!ok)
309
+ return null;
310
+ return Object.keys(variants).length === 0 ? null : { discriminant: 'type', variants };
311
+ }
312
+ async function collectSchemaVariants(typeName, source, filePath, ctx, variants, visitedAliases) {
313
+ const located = await findTypeSource(typeName, source, filePath, ctx, new Set());
314
+ if (!located)
315
+ return false;
316
+ const aliasKey = `${located.filePath}::${located.localName}`;
317
+ if (visitedAliases.has(aliasKey))
318
+ return true;
319
+ visitedAliases.add(aliasKey);
320
+ const sf = ts.createSourceFile(located.filePath, located.source, ts.ScriptTarget.Latest, true);
321
+ const aliases = [];
322
+ sf.forEachChild((n) => {
323
+ if (ts.isTypeAliasDeclaration(n))
324
+ aliases.push(n);
325
+ });
326
+ const alias = aliases.find((a) => a.name.text === located.localName);
327
+ if (!alias)
328
+ return false;
329
+ const memberNodes = ts.isUnionTypeNode(alias.type)
330
+ ? [...alias.type.types]
331
+ : [alias.type];
332
+ // Build a typeIndex that combines this file's local types with any
333
+ // *imported* type aliases referenced inside the variant payloads.
334
+ // Without this enrichment, a field typed as `GridSorting` (declared
335
+ // in `./state.ts` and imported here) would resolve to `'unknown'`
336
+ // because the local index doesn't know about it. The synthesizer
337
+ // would then emit `null` and the agent would have to guess at the
338
+ // permissible literal-union values.
339
+ const typeIndex = await buildEnrichedTypeIndex(sf, located.source, located.filePath, ctx);
340
+ for (const member of memberNodes) {
341
+ if (ts.isTypeLiteralNode(member)) {
342
+ collectOneVariant(member, variants, located.source, typeIndex);
343
+ continue;
344
+ }
345
+ if (ts.isTypeReferenceNode(member) && ts.isIdentifier(member.typeName)) {
346
+ await collectSchemaVariants(member.typeName.text, located.source, located.filePath, ctx, variants, visitedAliases);
347
+ continue;
348
+ }
349
+ }
350
+ return true;
351
+ }
352
+ function collectOneVariant(lit, variants, source, typeIndex) {
353
+ let discriminantValue = null;
354
+ const fields = {};
355
+ for (const member of lit.members) {
356
+ if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name))
357
+ continue;
358
+ const name = member.name.text;
359
+ const memberType = member.type;
360
+ if (name === 'type' && memberType) {
361
+ if (ts.isLiteralTypeNode(memberType) && ts.isStringLiteral(memberType.literal)) {
362
+ discriminantValue = memberType.literal.text;
363
+ }
364
+ continue;
365
+ }
366
+ fields[name] = buildFieldDescriptor(member, source, typeIndex);
367
+ }
368
+ if (discriminantValue && variants[discriminantValue] === undefined) {
369
+ variants[discriminantValue] = fields;
370
+ }
371
+ }
372
+ /**
373
+ * Build a TypeIndex that includes the locally-declared types in `sf`
374
+ * AND any types imported by name into `sf`. Following the imports
375
+ * picks up sibling-file aliases like `GridSorting`, `ScoreMode`,
376
+ * `ConfirmRequest` that an app commonly extracts to a state module.
377
+ *
378
+ * Limitations:
379
+ * - Only follows direct named imports (`import type { X } from './y'`).
380
+ * Namespace imports and `export *` aren't followed (the lint rule
381
+ * `agent-msg-resolvable` already catches the namespace case).
382
+ * - The resolved external type must itself be a type alias or
383
+ * interface in the target file — chained re-exports beyond the first
384
+ * hop fall back to `'unknown'`.
385
+ * - Best-effort: any failure to resolve an import is silent. The
386
+ * field type just stays `'unknown'` as it would have without
387
+ * enrichment.
388
+ */
389
+ async function buildEnrichedTypeIndex(sf, source, filePath, ctx) {
390
+ const index = new Map();
391
+ // 1. Locally-declared aliases / interfaces.
392
+ for (const stmt of sf.statements) {
393
+ if (ts.isTypeAliasDeclaration(stmt)) {
394
+ index.set(stmt.name.text, stmt.type);
395
+ }
396
+ else if (ts.isInterfaceDeclaration(stmt)) {
397
+ index.set(stmt.name.text, stmt);
398
+ }
399
+ }
400
+ // 2. Walk imports and resolve named-imported types via the resolver.
401
+ // Each successful resolve adds the target's declaration to the
402
+ // index under the local name. Type-only imports
403
+ // (`import type { X }`) are followed exactly the same as value
404
+ // imports — TypeScript's `isTypeOnly` flag doesn't change the
405
+ // referent.
406
+ for (const stmt of sf.statements) {
407
+ if (!ts.isImportDeclaration(stmt))
408
+ continue;
409
+ const named = stmt.importClause?.namedBindings;
410
+ if (!named || !ts.isNamedImports(named))
411
+ continue;
412
+ for (const spec of named.elements) {
413
+ const localName = spec.name.text;
414
+ const importedName = spec.propertyName?.text ?? localName;
415
+ if (index.has(localName))
416
+ continue;
417
+ const located = await findTypeSource(importedName, source, filePath, ctx, new Set());
418
+ if (!located)
419
+ continue;
420
+ const targetSf = ts.createSourceFile(located.filePath, located.source, ts.ScriptTarget.Latest, true);
421
+ for (const targetStmt of targetSf.statements) {
422
+ if (ts.isTypeAliasDeclaration(targetStmt) && targetStmt.name.text === located.localName) {
423
+ index.set(localName, targetStmt.type);
424
+ break;
425
+ }
426
+ if (ts.isInterfaceDeclaration(targetStmt) && targetStmt.name.text === located.localName) {
427
+ index.set(localName, targetStmt);
428
+ break;
429
+ }
430
+ }
431
+ }
432
+ }
433
+ return index;
434
+ }
435
+ /**
436
+ * Inspect the type arguments of a `component<...>()` call and return
437
+ * the textual identifier for each known position. Returns `null` for
438
+ * positions whose type argument isn't a plain identifier (e.g.
439
+ * inline literal types, generic instantiations, namespace-qualified
440
+ * names). Identifiers are what the resolver can chase; everything else
441
+ * we leave to the local extractor's existing behavior.
442
+ *
443
+ * Order: `[State, Msg, Effect]` matching `component<State, Msg, Effect>`.
444
+ */
445
+ export function readComponentTypeArgNames(call) {
446
+ const args = call.typeArguments;
447
+ const get = (i) => {
448
+ const t = args?.[i];
449
+ if (!t)
450
+ return null;
451
+ if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName))
452
+ return t.typeName.text;
453
+ return null;
454
+ };
455
+ return { state: get(0), msg: get(1), effect: get(2) };
456
+ }
457
+ //# sourceMappingURL=cross-file-resolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cross-file-resolver.js","sourceRoot":"","sources":["../src/cross-file-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript'\nimport {\n type MessageAnnotations,\n type DispatchMode as MessageDispatchMode,\n} from './msg-annotations.js'\nimport {\n type MsgSchema,\n type MsgField,\n type TypeIndex,\n buildFieldDescriptor,\n} from './msg-schema.js'\n\n/**\n * Cross-file type resolver.\n *\n * The schema/annotation extractors (`extractMsgAnnotations`,\n * `extractMsgSchema`, `extractStateSchema`, `extractEffectSchema`) only\n * see the source string for the file currently being transformed. When\n * a developer keeps the `Msg` (or `State` / `Effect`) union in a\n * separate file and imports it where `component()` is called, those\n * extractors silently return `null` — the plugin emits no annotations,\n * runtime LAP validation is disabled, and Claude can dispatch arbitrary\n * `type` strings that fall through to `assertNever`.\n *\n * This module follows imports and re-exports to find the source file\n * that declares the requested type alias, returning that file's source\n * string + the local name of the alias there. Extractors then run\n * against that source and produce the same output they would have for\n * a co-located declaration.\n *\n * Limitations:\n * - Composition (`type Msg = ImportedA | { type: 'b' }`): only the\n * locally-declared variants are extracted; the imported half isn't\n * walked recursively into. The lint rule `agent-msg-resolvable`\n * catches this case at lint time.\n * - Namespace imports (`import * as ns from './msg'`) and `export *`:\n * not followed. Same lint coverage.\n * - Generic types: not parameterized resolution; the type argument\n * must resolve to a concrete type alias.\n */\n\nexport interface ResolveContext {\n /**\n * Resolve a module specifier (e.g. `'./msg'`, `'@scope/pkg'`) against\n * the importing file's path. Returns the absolute filesystem path of\n * the resolved module, or `null` if it cannot be resolved (the type\n * stays unresolved and the extractor falls back to local-only mode).\n */\n resolveModule: (spec: string, importerPath: string) => Promise<string | null>\n /**\n * Read the source contents of an absolute module path. The contents\n * are parsed by TypeScript so they should be valid TS/TSX. The plugin\n *'s vite hook plumbs `fs/promises.readFile` here; tests provide an\n * in-memory map.\n */\n readSource: (absolutePath: string) => Promise<string>\n}\n\nexport interface ResolvedTypeSource {\n /** The full source string of the file declaring the type alias. */\n source: string\n /** The local name of the alias *in that file* (after rename chains). */\n localName: string\n /** Absolute path of the file declaring the alias (debug aid). */\n filePath: string\n}\n\n/**\n * Walk imports + re-exports to find where a type alias is actually\n * declared. Returns the source string and local name of the alias in\n * its declaring file. Returns `null` if the chain leads to an unresolved\n * module, a re-export through `export *`, a namespace import, or a\n * dead-end (alias not declared anywhere we can see).\n */\nexport async function findTypeSource(\n typeName: string,\n source: string,\n filePath: string,\n ctx: ResolveContext,\n visited: Set<string> = new Set(),\n): Promise<ResolvedTypeSource | null> {\n // Cycle prevention — re-export A → A is a tight loop that some\n // pathological re-export chains can produce. Bail rather than\n // infinitely recurse.\n if (visited.has(`${filePath}::${typeName}`)) return null\n visited.add(`${filePath}::${typeName}`)\n\n const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true)\n\n // 1. Local declaration wins. `type X = ...` or `interface X { ... }`\n // (extractors only support type aliases today, but check both so\n // the resolver itself isn't a footgun for future extractors).\n for (const stmt of sf.statements) {\n if (ts.isTypeAliasDeclaration(stmt) && stmt.name.text === typeName) {\n return { source, localName: typeName, filePath }\n }\n if (ts.isInterfaceDeclaration(stmt) && stmt.name.text === typeName) {\n return { source, localName: typeName, filePath }\n }\n }\n\n // 2. Re-export with name: `export { X } from './y'` or\n // `export { X as Y } from './y'`. Walk to the source module.\n for (const stmt of sf.statements) {\n if (!ts.isExportDeclaration(stmt)) continue\n if (!stmt.exportClause || !ts.isNamedExports(stmt.exportClause)) continue\n if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n\n for (const spec of stmt.exportClause.elements) {\n const exportedName = spec.name.text\n if (exportedName !== typeName) continue\n // The name in the source module is `propertyName` if present\n // (e.g. `export { Msg as M } from './msg'` exports as M but the\n // source module has it as Msg).\n const sourceName = spec.propertyName?.text ?? spec.name.text\n const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath)\n if (!resolved) return null\n const subSource = await ctx.readSource(resolved)\n return findTypeSource(sourceName, subSource, resolved, ctx, visited)\n }\n }\n\n // 3. Local re-binding: `export { X } from elsewhere` shorthand was\n // handled above. A separate case is `import { X } from ... ; export\n // { X }` — the import already declares X locally, so step 5 picks\n // it up.\n\n // 4. Star re-exports: `export * from './y'`. The barrel re-exports\n // every named member of `./y` under the same name. Walk each\n // barrel target and return the first hit. Order: textual order\n // in the source file (matches TypeScript's behaviour for\n // multi-barrel name collisions, where the first declared wins).\n //\n // Multiple `export *` declarations are common in monorepo barrel\n // files (`export * from './msg'; export * from './effects'`).\n // Without this step, the resolver returns `null` and the plugin\n // silently emits empty annotations for any consumer that points\n // at a barrel.\n for (const stmt of sf.statements) {\n if (!ts.isExportDeclaration(stmt)) continue\n // `export * from './y'` has no exportClause; `export {} from './y'`\n // is a different beast (re-exports nothing). Skip the latter.\n if (stmt.exportClause) continue\n if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n\n const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath)\n if (!resolved) continue\n let subSource: string\n try {\n subSource = await ctx.readSource(resolved)\n } catch {\n // Module path resolved but the file isn't readable (deleted,\n // dynamic-only, etc.). Continue to the next barrel.\n continue\n }\n const found = await findTypeSource(typeName, subSource, resolved, ctx, visited)\n if (found) return found\n }\n\n // 5. Imports: `import { X } from './y'` or `import { X as Y } from './y'`.\n // Walk to the source module using the original (imported) name.\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt)) continue\n if (!stmt.importClause) continue\n if (!stmt.moduleSpecifier || !ts.isStringLiteral(stmt.moduleSpecifier)) continue\n\n const bindings = stmt.importClause.namedBindings\n if (!bindings || !ts.isNamedImports(bindings)) continue\n\n for (const elem of bindings.elements) {\n const localName = elem.name.text\n if (localName !== typeName) continue\n // The remote name is `propertyName` when there's a rename, else\n // the local name itself.\n const remoteName = elem.propertyName?.text ?? elem.name.text\n const resolved = await ctx.resolveModule(stmt.moduleSpecifier.text, filePath)\n if (!resolved) return null\n const subSource = await ctx.readSource(resolved)\n return findTypeSource(remoteName, subSource, resolved, ctx, visited)\n }\n }\n\n // Not found in this file and no import/re-export to follow.\n return null\n}\n\n/**\n * Annotation extractor that walks composed Msg unions across files.\n *\n * Given a Msg type that may be a union of inline `{ type: 'literal' }`\n * objects AND TypeReferences (e.g.\n * `type Msg = ImportedFoo | { type: 'extra' }`), recursively follow\n * each TypeReference via `findTypeSource` and merge its variants into\n * the returned map.\n *\n * Composition + cross-file is the union of two failure modes the\n * file-local sync extractor silently mishandles. This function\n * produces the same map the runtime expects regardless of how the\n * developer organized the type declarations.\n *\n * Conflict policy: if two composed branches contribute the same\n * discriminant string (e.g. both halves declare `{ type: 'inc' }`),\n * the first one walked wins. The lint rule `agent-msg-resolvable`\n * fires before this point on most pathological cases; ESLint's\n * type-checker would flag the duplicate independently.\n */\nexport async function extractMsgAnnotationsCrossFile(\n source: string,\n typeName: string,\n filePath: string,\n ctx: ResolveContext,\n): Promise<Record<string, MessageAnnotations> | null> {\n const out: Record<string, MessageAnnotations> = {}\n const ok = await collectMsgVariants(typeName, source, filePath, ctx, out, new Set())\n if (!ok) return null\n return Object.keys(out).length === 0 ? null : out\n}\n\nasync function collectMsgVariants(\n typeName: string,\n source: string,\n filePath: string,\n ctx: ResolveContext,\n out: Record<string, MessageAnnotations>,\n visitedAliases: Set<string>,\n): Promise<boolean> {\n const located = await findTypeSource(typeName, source, filePath, ctx, new Set())\n if (!located) return false\n\n const aliasKey = `${located.filePath}::${located.localName}`\n if (visitedAliases.has(aliasKey)) return true\n visitedAliases.add(aliasKey)\n\n const sf = ts.createSourceFile(located.filePath, located.source, ts.ScriptTarget.Latest, true)\n const aliases: ts.TypeAliasDeclaration[] = []\n sf.forEachChild((n) => {\n if (ts.isTypeAliasDeclaration(n)) aliases.push(n)\n })\n const alias = aliases.find((a) => a.name.text === located.localName)\n if (!alias) return false\n\n // Single-variant alias: `type Foo = { type: 'a', ... }`. Treat as a\n // one-element union so a Msg variant can be its own type alias.\n const memberNodes: ts.TypeNode[] = ts.isUnionTypeNode(alias.type)\n ? [...alias.type.types]\n : [alias.type]\n\n for (let i = 0; i < memberNodes.length; i++) {\n const member = memberNodes[i]!\n\n if (ts.isTypeLiteralNode(member)) {\n const variant = readDiscriminantLiteral(member)\n if (!variant) continue\n const comment = readLeadingJSDocForMember(located.source, alias, memberNodes, i)\n if (out[variant] === undefined) {\n out[variant] = parseMessageAnnotations(comment)\n }\n continue\n }\n\n if (ts.isTypeReferenceNode(member) && ts.isIdentifier(member.typeName)) {\n // Composed: recurse through the resolver.\n await collectMsgVariants(\n member.typeName.text,\n located.source,\n located.filePath,\n ctx,\n out,\n visitedAliases,\n )\n continue\n }\n\n // Other shapes (intersections, conditional types, namespace-qualified\n // names) aren't followed. Lint catches this.\n }\n\n return true\n}\n\nfunction readDiscriminantLiteral(lit: ts.TypeLiteralNode): string | null {\n for (const m of lit.members) {\n if (!ts.isPropertySignature(m)) continue\n if (!m.name || !ts.isIdentifier(m.name) || m.name.text !== 'type') continue\n if (!m.type || !ts.isLiteralTypeNode(m.type)) continue\n const literal = m.type.literal\n if (ts.isStringLiteral(literal)) return literal.text\n }\n return null\n}\n\n/**\n * Read leading JSDoc for a union member at index `i` of `members`.\n * The JSDoc lives between the previous element's end and the current\n * element's start (or between the type alias start and the first\n * element for `i === 0`). Mirrors the logic in\n * `extractMsgAnnotations` so the cross-file path produces the same\n * output for the same input.\n */\nfunction readLeadingJSDocForMember(\n source: string,\n alias: ts.TypeAliasDeclaration,\n members: ts.TypeNode[],\n i: number,\n): string {\n const prev = members[i - 1]\n const member = members[i]!\n // For non-union (single-variant) aliases the union pos is the alias\n // body's pos.\n const unionPos = ts.isUnionTypeNode(alias.type) ? alias.type.pos : alias.type.pos\n const scanPos = i === 0 || prev === undefined ? unionPos : prev.end\n const ranges = ts.getLeadingCommentRanges(source, scanPos) ?? []\n const docs = ranges\n .filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)\n .map((r) => source.slice(r.pos, r.end))\n .filter((txt) => txt.startsWith('/**'))\n // Cut off comments that appear AFTER the member starts (rare but\n // possible with weird formatting).\n const _end = member.pos\n return docs.join('\\n')\n}\n\nfunction parseMessageAnnotations(comment: string): MessageAnnotations {\n if (!comment) return defaultMessageAnnotations()\n const intent = readIntentTag(comment)\n const human = /@humanOnly\\b/.test(comment)\n const agent = /@agentOnly\\b/.test(comment)\n const dispatchMode: MessageDispatchMode =\n human && !agent ? 'human-only' : agent && !human ? 'agent-only' : 'shared'\n return {\n intent,\n alwaysAffordable: /@alwaysAffordable\\b/.test(comment),\n requiresConfirm: /@requiresConfirm\\b/.test(comment),\n dispatchMode,\n examples: readExamplesTag(comment),\n warning: readWarningTag(comment),\n emits: readEmitsTag(comment),\n }\n}\n\nfunction readEmitsTag(comment: string): string[] {\n const outer = comment.match(/@emits\\s*\\(([^)]*)\\)/)\n if (!outer || outer[1] === undefined) return []\n const inner = outer[1]\n const seen = new Set<string>()\n const out: string[] = []\n const re = /[\"“]([^\"”]*)[\"”]/g\n let m: RegExpExecArray | null\n while ((m = re.exec(inner)) !== null) {\n const v = m[1]\n if (v === undefined || seen.has(v)) continue\n seen.add(v)\n out.push(v)\n }\n return out\n}\n\nfunction readExamplesTag(comment: string): string[] {\n const out: string[] = []\n const re = /@example\\s*\\(\\s*[\"“]([^\"”]*)[\"”]\\s*\\)/g\n let m: RegExpExecArray | null\n while ((m = re.exec(comment)) !== null) {\n if (m[1] !== undefined) out.push(m[1])\n }\n return out\n}\n\nfunction readWarningTag(comment: string): string | null {\n const match = comment.match(/@warning\\s*\\(\\s*[\"“]([^\"”]*)[\"”]\\s*\\)/)\n return match?.[1] ?? null\n}\n\nfunction defaultMessageAnnotations(): MessageAnnotations {\n return {\n intent: null,\n alwaysAffordable: false,\n requiresConfirm: false,\n dispatchMode: 'shared',\n examples: [],\n warning: null,\n emits: [],\n }\n}\n\nfunction readIntentTag(comment: string): string | null {\n const match = comment.match(/@intent\\s*\\(\\s*[\"“]([^\"”]*)[\"”]\\s*\\)/)\n return match?.[1] ?? null\n}\n\n/**\n * Cross-file companion to `extractMsgSchema` / `extractEffectSchema`.\n *\n * Discriminated-union schema extractor that follows composed\n * TypeReferences through the resolver. Same recursion shape as\n * `extractMsgAnnotationsCrossFile`, just collecting field shapes\n * instead of JSDoc annotations.\n */\nexport async function extractDiscriminatedUnionSchemaCrossFile(\n source: string,\n typeName: string,\n filePath: string,\n ctx: ResolveContext,\n): Promise<MsgSchema | null> {\n const variants: MsgSchema['variants'] = {}\n const ok = await collectSchemaVariants(typeName, source, filePath, ctx, variants, new Set())\n if (!ok) return null\n return Object.keys(variants).length === 0 ? null : { discriminant: 'type', variants }\n}\n\nasync function collectSchemaVariants(\n typeName: string,\n source: string,\n filePath: string,\n ctx: ResolveContext,\n variants: MsgSchema['variants'],\n visitedAliases: Set<string>,\n): Promise<boolean> {\n const located = await findTypeSource(typeName, source, filePath, ctx, new Set())\n if (!located) return false\n\n const aliasKey = `${located.filePath}::${located.localName}`\n if (visitedAliases.has(aliasKey)) return true\n visitedAliases.add(aliasKey)\n\n const sf = ts.createSourceFile(located.filePath, located.source, ts.ScriptTarget.Latest, true)\n const aliases: ts.TypeAliasDeclaration[] = []\n sf.forEachChild((n) => {\n if (ts.isTypeAliasDeclaration(n)) aliases.push(n)\n })\n const alias = aliases.find((a) => a.name.text === located.localName)\n if (!alias) return false\n\n const memberNodes: ts.TypeNode[] = ts.isUnionTypeNode(alias.type)\n ? [...alias.type.types]\n : [alias.type]\n\n // Build a typeIndex that combines this file's local types with any\n // *imported* type aliases referenced inside the variant payloads.\n // Without this enrichment, a field typed as `GridSorting` (declared\n // in `./state.ts` and imported here) would resolve to `'unknown'`\n // because the local index doesn't know about it. The synthesizer\n // would then emit `null` and the agent would have to guess at the\n // permissible literal-union values.\n const typeIndex = await buildEnrichedTypeIndex(sf, located.source, located.filePath, ctx)\n\n for (const member of memberNodes) {\n if (ts.isTypeLiteralNode(member)) {\n collectOneVariant(member, variants, located.source, typeIndex)\n continue\n }\n if (ts.isTypeReferenceNode(member) && ts.isIdentifier(member.typeName)) {\n await collectSchemaVariants(\n member.typeName.text,\n located.source,\n located.filePath,\n ctx,\n variants,\n visitedAliases,\n )\n continue\n }\n }\n return true\n}\n\nfunction collectOneVariant(\n lit: ts.TypeLiteralNode,\n variants: MsgSchema['variants'],\n source: string,\n typeIndex: TypeIndex,\n): void {\n let discriminantValue: string | null = null\n const fields: Record<string, MsgField> = {}\n for (const member of lit.members) {\n if (!ts.isPropertySignature(member) || !member.name || !ts.isIdentifier(member.name)) continue\n const name = member.name.text\n const memberType = member.type\n if (name === 'type' && memberType) {\n if (ts.isLiteralTypeNode(memberType) && ts.isStringLiteral(memberType.literal)) {\n discriminantValue = memberType.literal.text\n }\n continue\n }\n fields[name] = buildFieldDescriptor(member, source, typeIndex)\n }\n if (discriminantValue && variants[discriminantValue] === undefined) {\n variants[discriminantValue] = fields\n }\n}\n\n/**\n * Build a TypeIndex that includes the locally-declared types in `sf`\n * AND any types imported by name into `sf`. Following the imports\n * picks up sibling-file aliases like `GridSorting`, `ScoreMode`,\n * `ConfirmRequest` that an app commonly extracts to a state module.\n *\n * Limitations:\n * - Only follows direct named imports (`import type { X } from './y'`).\n * Namespace imports and `export *` aren't followed (the lint rule\n * `agent-msg-resolvable` already catches the namespace case).\n * - The resolved external type must itself be a type alias or\n * interface in the target file — chained re-exports beyond the first\n * hop fall back to `'unknown'`.\n * - Best-effort: any failure to resolve an import is silent. The\n * field type just stays `'unknown'` as it would have without\n * enrichment.\n */\nasync function buildEnrichedTypeIndex(\n sf: ts.SourceFile,\n source: string,\n filePath: string,\n ctx: ResolveContext,\n): Promise<TypeIndex> {\n const index: TypeIndex = new Map()\n\n // 1. Locally-declared aliases / interfaces.\n for (const stmt of sf.statements) {\n if (ts.isTypeAliasDeclaration(stmt)) {\n index.set(stmt.name.text, stmt.type)\n } else if (ts.isInterfaceDeclaration(stmt)) {\n index.set(stmt.name.text, stmt)\n }\n }\n\n // 2. Walk imports and resolve named-imported types via the resolver.\n // Each successful resolve adds the target's declaration to the\n // index under the local name. Type-only imports\n // (`import type { X }`) are followed exactly the same as value\n // imports — TypeScript's `isTypeOnly` flag doesn't change the\n // referent.\n for (const stmt of sf.statements) {\n if (!ts.isImportDeclaration(stmt)) continue\n const named = stmt.importClause?.namedBindings\n if (!named || !ts.isNamedImports(named)) continue\n for (const spec of named.elements) {\n const localName = spec.name.text\n const importedName = spec.propertyName?.text ?? localName\n if (index.has(localName)) continue\n const located = await findTypeSource(importedName, source, filePath, ctx, new Set())\n if (!located) continue\n const targetSf = ts.createSourceFile(\n located.filePath,\n located.source,\n ts.ScriptTarget.Latest,\n true,\n )\n for (const targetStmt of targetSf.statements) {\n if (ts.isTypeAliasDeclaration(targetStmt) && targetStmt.name.text === located.localName) {\n index.set(localName, targetStmt.type)\n break\n }\n if (ts.isInterfaceDeclaration(targetStmt) && targetStmt.name.text === located.localName) {\n index.set(localName, targetStmt)\n break\n }\n }\n }\n }\n\n return index\n}\n\n/**\n * Inspect the type arguments of a `component<...>()` call and return\n * the textual identifier for each known position. Returns `null` for\n * positions whose type argument isn't a plain identifier (e.g.\n * inline literal types, generic instantiations, namespace-qualified\n * names). Identifiers are what the resolver can chase; everything else\n * we leave to the local extractor's existing behavior.\n *\n * Order: `[State, Msg, Effect]` matching `component<State, Msg, Effect>`.\n */\nexport function readComponentTypeArgNames(call: ts.CallExpression): {\n state: string | null\n msg: string | null\n effect: string | null\n} {\n const args = call.typeArguments\n const get = (i: number): string | null => {\n const t = args?.[i]\n if (!t) return null\n if (ts.isTypeReferenceNode(t) && ts.isIdentifier(t.typeName)) return t.typeName.text\n return null\n }\n return { state: get(0), msg: get(1), effect: get(2) }\n}\n"]}