@llui/vite-plugin 0.0.28 → 0.0.30

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,91 @@
1
+ import ts from 'typescript';
2
+ const DEFAULT = {
3
+ intent: null,
4
+ alwaysAffordable: false,
5
+ requiresConfirm: false,
6
+ humanOnly: false,
7
+ };
8
+ /**
9
+ * Walk a Msg-like discriminated-union type alias and extract JSDoc
10
+ * annotations attached to each union member. Returns null if no
11
+ * recognizable union is found so callers can skip emission cleanly.
12
+ *
13
+ * Expected JSDoc grammar (order-independent):
14
+ * @intent("human readable")
15
+ * @alwaysAffordable
16
+ * @requiresConfirm
17
+ * @humanOnly
18
+ *
19
+ * Unknown tags are ignored; malformed @intent (no quoted string) is
20
+ * treated as "no intent". The four flags are booleans; any occurrence
21
+ * of the tag sets it true.
22
+ */
23
+ export function extractMsgAnnotations(source) {
24
+ const sf = ts.createSourceFile('msg.ts', source, ts.ScriptTarget.Latest, true);
25
+ const aliases = [];
26
+ sf.forEachChild((n) => {
27
+ if (ts.isTypeAliasDeclaration(n))
28
+ aliases.push(n);
29
+ });
30
+ const named = aliases.find((a) => a.name.text === 'Msg');
31
+ const alias = named ?? aliases.find((a) => ts.isUnionTypeNode(a.type));
32
+ if (!alias || !ts.isUnionTypeNode(alias.type))
33
+ return null;
34
+ const result = {};
35
+ const types = alias.type.types;
36
+ for (let i = 0; i < types.length; i++) {
37
+ const member = types[i];
38
+ if (member === undefined || !ts.isTypeLiteralNode(member))
39
+ continue;
40
+ const variant = readDiscriminantLiteral(member);
41
+ if (!variant)
42
+ continue;
43
+ // Leading JSDoc for union member i is scanned from the end of the
44
+ // previous element (or union.pos for the first member), because
45
+ // TypeScript's parser places comment ranges relative to the token
46
+ // that follows them — and the | bar is not part of the TypeLiteralNode.
47
+ const prev = types[i - 1];
48
+ const scanPos = i === 0 || prev === undefined ? alias.type.pos : prev.end;
49
+ const comment = readLeadingJSDoc(source, scanPos);
50
+ result[variant] = parseAnnotations(comment);
51
+ }
52
+ return Object.keys(result).length === 0 ? null : result;
53
+ }
54
+ function readDiscriminantLiteral(lit) {
55
+ for (const m of lit.members) {
56
+ if (!ts.isPropertySignature(m))
57
+ continue;
58
+ if (!m.name || !ts.isIdentifier(m.name) || m.name.text !== 'type')
59
+ continue;
60
+ if (!m.type || !ts.isLiteralTypeNode(m.type))
61
+ continue;
62
+ const literal = m.type.literal;
63
+ if (ts.isStringLiteral(literal))
64
+ return literal.text;
65
+ }
66
+ return null;
67
+ }
68
+ function readLeadingJSDoc(source, scanPos) {
69
+ const ranges = ts.getLeadingCommentRanges(source, scanPos) ?? [];
70
+ const docs = ranges
71
+ .filter((r) => r.kind === ts.SyntaxKind.MultiLineCommentTrivia)
72
+ .map((r) => source.slice(r.pos, r.end))
73
+ .filter((txt) => txt.startsWith('/**'));
74
+ return docs.join('\n');
75
+ }
76
+ function parseAnnotations(comment) {
77
+ if (!comment)
78
+ return { ...DEFAULT };
79
+ const intent = readIntent(comment);
80
+ return {
81
+ intent,
82
+ alwaysAffordable: /@alwaysAffordable\b/.test(comment),
83
+ requiresConfirm: /@requiresConfirm\b/.test(comment),
84
+ humanOnly: /@humanOnly\b/.test(comment),
85
+ };
86
+ }
87
+ function readIntent(comment) {
88
+ const match = comment.match(/@intent\s*\(\s*["\u201c]([^"\u201d]*)["\u201d]\s*\)/);
89
+ return match?.[1] ?? null;
90
+ }
91
+ //# sourceMappingURL=msg-annotations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"msg-annotations.js","sourceRoot":"","sources":["../src/msg-annotations.ts"],"names":[],"mappings":"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","sourcesContent":["import ts from 'typescript'\n\nexport type MessageAnnotations = {\n intent: string | null\n alwaysAffordable: boolean\n requiresConfirm: boolean\n humanOnly: boolean\n}\n\nconst DEFAULT: MessageAnnotations = {\n intent: null,\n alwaysAffordable: false,\n requiresConfirm: false,\n humanOnly: false,\n}\n\n/**\n * Walk a Msg-like discriminated-union type alias and extract JSDoc\n * annotations attached to each union member. Returns null if no\n * recognizable union is found so callers can skip emission cleanly.\n *\n * Expected JSDoc grammar (order-independent):\n * @intent(\"human readable\")\n * @alwaysAffordable\n * @requiresConfirm\n * @humanOnly\n *\n * Unknown tags are ignored; malformed @intent (no quoted string) is\n * treated as \"no intent\". The four flags are booleans; any occurrence\n * of the tag sets it true.\n */\nexport function extractMsgAnnotations(source: string): Record<string, MessageAnnotations> | null {\n const sf = ts.createSourceFile('msg.ts', 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 named = aliases.find((a) => a.name.text === 'Msg')\n const alias = named ?? aliases.find((a) => ts.isUnionTypeNode(a.type))\n if (!alias || !ts.isUnionTypeNode(alias.type)) return null\n\n const result: Record<string, MessageAnnotations> = {}\n const types = alias.type.types\n for (let i = 0; i < types.length; i++) {\n const member = types[i]\n if (member === undefined || !ts.isTypeLiteralNode(member)) continue\n const variant = readDiscriminantLiteral(member)\n if (!variant) continue\n // Leading JSDoc for union member i is scanned from the end of the\n // previous element (or union.pos for the first member), because\n // TypeScript's parser places comment ranges relative to the token\n // that follows them — and the | bar is not part of the TypeLiteralNode.\n const prev = types[i - 1]\n const scanPos = i === 0 || prev === undefined ? alias.type.pos : prev.end\n const comment = readLeadingJSDoc(source, scanPos)\n result[variant] = parseAnnotations(comment)\n }\n return Object.keys(result).length === 0 ? null : result\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\nfunction readLeadingJSDoc(source: string, scanPos: number): string {\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 return docs.join('\\n')\n}\n\nfunction parseAnnotations(comment: string): MessageAnnotations {\n if (!comment) return { ...DEFAULT }\n const intent = readIntent(comment)\n return {\n intent,\n alwaysAffordable: /@alwaysAffordable\\b/.test(comment),\n requiresConfirm: /@requiresConfirm\\b/.test(comment),\n humanOnly: /@humanOnly\\b/.test(comment),\n }\n}\n\nfunction readIntent(comment: string): string | null {\n const match = comment.match(/@intent\\s*\\(\\s*[\"\\u201c]([^\"\\u201d]*)[\"\\u201d]\\s*\\)/)\n return match?.[1] ?? null\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import type { MessageAnnotations } from './msg-annotations.js';
2
+ export type SchemaHashInput = {
3
+ msgSchema: unknown;
4
+ stateSchema: unknown;
5
+ msgAnnotations: Record<string, MessageAnnotations> | null | undefined;
6
+ };
7
+ /**
8
+ * Stable hex SHA-256 (first 32 chars) over a normalized JSON serialization
9
+ * of msgSchema + stateSchema + msgAnnotations. Object key order is
10
+ * normalized so equivalent inputs always produce equal hashes.
11
+ *
12
+ * Used by the runtime to detect when the browser-to-server `hello` frame
13
+ * needs to re-send its schema payload (dev hot-reload).
14
+ */
15
+ export declare function computeSchemaHash(input: SchemaHashInput): string;
16
+ //# sourceMappingURL=schema-hash.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-hash.d.ts","sourceRoot":"","sources":["../src/schema-hash.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAA;AAE9D,MAAM,MAAM,eAAe,GAAG;IAC5B,SAAS,EAAE,OAAO,CAAA;IAClB,WAAW,EAAE,OAAO,CAAA;IACpB,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,GAAG,IAAI,GAAG,SAAS,CAAA;CACtE,CAAA;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAQhE"}
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'node:crypto';
2
+ /**
3
+ * Stable hex SHA-256 (first 32 chars) over a normalized JSON serialization
4
+ * of msgSchema + stateSchema + msgAnnotations. Object key order is
5
+ * normalized so equivalent inputs always produce equal hashes.
6
+ *
7
+ * Used by the runtime to detect when the browser-to-server `hello` frame
8
+ * needs to re-send its schema payload (dev hot-reload).
9
+ */
10
+ export function computeSchemaHash(input) {
11
+ const normalized = {
12
+ msgSchema: sortDeep(input.msgSchema),
13
+ stateSchema: sortDeep(input.stateSchema),
14
+ msgAnnotations: sortDeep(input.msgAnnotations ?? null),
15
+ };
16
+ const json = JSON.stringify(normalized);
17
+ return createHash('sha256').update(json).digest('hex').slice(0, 32);
18
+ }
19
+ function sortDeep(value) {
20
+ if (value === null || typeof value !== 'object')
21
+ return value;
22
+ if (Array.isArray(value))
23
+ return value.map(sortDeep);
24
+ const obj = value;
25
+ const out = {};
26
+ for (const k of Object.keys(obj).sort()) {
27
+ out[k] = sortDeep(obj[k]);
28
+ }
29
+ return out;
30
+ }
31
+ //# sourceMappingURL=schema-hash.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema-hash.js","sourceRoot":"","sources":["../src/schema-hash.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AASxC;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAAsB;IACtD,MAAM,UAAU,GAAG;QACjB,SAAS,EAAE,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC;QACpC,WAAW,EAAE,QAAQ,CAAC,KAAK,CAAC,WAAW,CAAC;QACxC,cAAc,EAAE,QAAQ,CAAC,KAAK,CAAC,cAAc,IAAI,IAAI,CAAC;KACvD,CAAA;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAA;IACvC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;AACrE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAA;IAC7D,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACpD,MAAM,GAAG,GAAG,KAAgC,CAAA;IAC5C,MAAM,GAAG,GAA4B,EAAE,CAAA;IACvC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QACxC,GAAG,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;IAC3B,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC","sourcesContent":["import { createHash } from 'node:crypto'\nimport type { MessageAnnotations } from './msg-annotations.js'\n\nexport type SchemaHashInput = {\n msgSchema: unknown\n stateSchema: unknown\n msgAnnotations: Record<string, MessageAnnotations> | null | undefined\n}\n\n/**\n * Stable hex SHA-256 (first 32 chars) over a normalized JSON serialization\n * of msgSchema + stateSchema + msgAnnotations. Object key order is\n * normalized so equivalent inputs always produce equal hashes.\n *\n * Used by the runtime to detect when the browser-to-server `hello` frame\n * needs to re-send its schema payload (dev hot-reload).\n */\nexport function computeSchemaHash(input: SchemaHashInput): string {\n const normalized = {\n msgSchema: sortDeep(input.msgSchema),\n stateSchema: sortDeep(input.stateSchema),\n msgAnnotations: sortDeep(input.msgAnnotations ?? null),\n }\n const json = JSON.stringify(normalized)\n return createHash('sha256').update(json).digest('hex').slice(0, 32)\n}\n\nfunction sortDeep(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value\n if (Array.isArray(value)) return value.map(sortDeep)\n const obj = value as Record<string, unknown>\n const out: Record<string, unknown> = {}\n for (const k of Object.keys(obj).sort()) {\n out[k] = sortDeep(obj[k])\n }\n return out\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import ts from 'typescript';
1
2
  /**
2
3
  * Transform a source file containing @llui/dom imports.
3
4
  * Returns the transformed source or null if no transformation needed.
@@ -7,7 +8,7 @@ export interface TransformEdit {
7
8
  end: number;
8
9
  replacement: string;
9
10
  }
10
- export declare function transformLlui(source: string, _filename: string, devMode?: boolean, mcpPort?: number | null, verbose?: boolean): {
11
+ export declare function transformLlui(source: string, _filename: string, devMode?: boolean, emitAgentMetadata?: boolean, mcpPort?: number | null, verbose?: boolean): {
11
12
  output: string;
12
13
  edits: TransformEdit[];
13
14
  } | null;
@@ -47,4 +48,31 @@ export declare function transformUseClientSsr(source: string, _filename: string)
47
48
  * without parsing the whole file twice.
48
49
  */
49
50
  export declare function hasUseClientDirective(source: string): boolean;
51
+ /**
52
+ * Extract the view function body (the value of the `view:` property) from
53
+ * a component() config object literal. Uses a regex heuristic — good enough
54
+ * for round-tripping source for dev/agent tools.
55
+ */
56
+ export declare function extractViewBody(code: string): string | null;
57
+ /**
58
+ * Extract the component `name:` string literal from a component() call's
59
+ * first argument object literal in the source text.
60
+ */
61
+ export declare function extractComponentNameFromConfig(node: ts.CallExpression): string | null;
62
+ /**
63
+ * Generate Object.defineProperty calls for __preSource, __postSource,
64
+ * __msgMaskMap, and __bindingSources on a component variable. These are
65
+ * non-enumerable so they don't appear in JSON.stringify(componentDef) but are
66
+ * visible to devtools.
67
+ */
68
+ export declare function generateCompilerCacheProps(varName: string, componentName: string): string;
69
+ /**
70
+ * After the full output string is assembled, update each cached component's
71
+ * postSource (extract view body from the transformed output), then append
72
+ * Object.defineProperty calls for all four compiler-cache properties.
73
+ */
74
+ export declare function appendCompilerCacheProps(output: string, componentDecls: Array<{
75
+ varName: string;
76
+ componentName: string;
77
+ }>): string;
50
78
  //# sourceMappingURL=transform.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AA6KA;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,UAAQ,EACf,OAAO,GAAE,MAAM,GAAG,IAAW,EAC7B,OAAO,UAAQ,GACd;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CAuQnD;AA++HD,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,wBAAwB,GAAG,IAAI,CAoGjC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4B7D"}
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../src/transform.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,YAAY,CAAA;AAiL3B;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,wBAAgB,aAAa,CAC3B,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,EACjB,OAAO,UAAQ,EACf,iBAAiB,UAAQ,EACzB,OAAO,GAAE,MAAM,GAAG,IAAW,EAC7B,OAAO,UAAQ,GACd;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG,IAAI,CA8TnD;AAwoID,MAAM,WAAW,wBAAwB;IACvC,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,MAAM,EAAE,CAAA;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,SAAS,EAAE,MAAM,GAChB,wBAAwB,GAAG,IAAI,CAoGjC;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CA4B7D;AAID;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAI3D;AAED;;;GAGG;AACH,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,GAAG,MAAM,GAAG,IAAI,CAcrF;AAED;;;;;GAKG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,MAAM,CASzF;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,KAAK,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAA;CAAE,CAAC,GAChE,MAAM,CAYR"}
package/dist/transform.js CHANGED
@@ -1,7 +1,11 @@
1
1
  import ts from 'typescript';
2
2
  import { collectDeps } from './collect-deps.js';
3
3
  import { extractMsgSchema, extractEffectSchema } from './msg-schema.js';
4
+ import { extractMsgAnnotations } from './msg-annotations.js';
4
5
  import { extractStateSchema } from './state-schema.js';
6
+ import { computeSchemaHash } from './schema-hash.js';
7
+ import { extractBindingDescriptors } from './binding-descriptors.js';
8
+ import { compilerCache } from './compiler-cache.js';
5
9
  function createMaskLiteral(f, mask) {
6
10
  if (mask >= 0)
7
11
  return f.createNumericLiteral(mask);
@@ -172,7 +176,7 @@ function resolveKey(key, kind) {
172
176
  return 'class';
173
177
  return key;
174
178
  }
175
- export function transformLlui(source, _filename, devMode = false, mcpPort = 5200, verbose = false) {
179
+ export function transformLlui(source, _filename, devMode = false, emitAgentMetadata = false, mcpPort = 5200, verbose = false) {
176
180
  const sourceFile = ts.createSourceFile('input.ts', source, ts.ScriptTarget.Latest, true);
177
181
  // Find the @llui/dom import
178
182
  const imp = findLluiImport(sourceFile);
@@ -296,12 +300,20 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
296
300
  let result = tryInjectDirty(node, fieldBits, f);
297
301
  if (result)
298
302
  usesApplyBinding = true;
299
- if (devMode) {
300
- const schema = extractMsgSchema(source);
301
- if (schema) {
302
- result = injectMsgSchema(result ?? node, schema, f);
303
+ // Extract schema data once — used both for devMode injections and the
304
+ // unconditional __schemaHash (spec §7.4: hash ships in prod too).
305
+ const msgSchema = extractMsgSchema(source);
306
+ const msgAnnotations = extractMsgAnnotations(source);
307
+ const stateSchema = extractStateSchema(source);
308
+ const bindingDescriptors = extractBindingDescriptors(source);
309
+ const shouldEmitAgentMetadata = devMode || emitAgentMetadata;
310
+ if (shouldEmitAgentMetadata) {
311
+ if (msgSchema) {
312
+ result = injectMsgSchema(result ?? node, msgSchema, f);
313
+ }
314
+ if (msgAnnotations && hasNonDefaultAnnotation(msgAnnotations)) {
315
+ result = injectMsgAnnotations(result ?? node, msgAnnotations, f);
303
316
  }
304
- const stateSchema = extractStateSchema(source);
305
317
  if (stateSchema) {
306
318
  result = injectStateSchema(result ?? node, stateSchema.fields, f);
307
319
  }
@@ -309,8 +321,37 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
309
321
  if (effectSchema) {
310
322
  result = injectEffectSchema(result ?? node, effectSchema, f);
311
323
  }
324
+ if (bindingDescriptors.length > 0) {
325
+ result = injectBindingDescriptors(result ?? node, bindingDescriptors, f);
326
+ }
327
+ // Populate compiler cache — preSource and msgMaskMap are known now;
328
+ // postSource is filled in after the full output is assembled.
329
+ const cachedComponentName = extractComponentNameFromConfig(node);
330
+ if (cachedComponentName) {
331
+ const preSource = extractViewBody(source) ?? '';
332
+ const msgMaskMap = {};
333
+ for (const [path, bit] of fieldBits) {
334
+ msgMaskMap[path] = bit;
335
+ }
336
+ compilerCache.set(cachedComponentName, {
337
+ preSource,
338
+ postSource: '',
339
+ msgMaskMap,
340
+ bindingSources: [],
341
+ });
342
+ }
343
+ }
344
+ if (devMode) {
312
345
  result = injectComponentMeta(result ?? node, node, sourceFile, _filename, f);
313
346
  }
347
+ // __schemaHash is always emitted (not dev-gated) — runtime uses it to
348
+ // gate hello-frame re-sends during hot-reload in prod builds too.
349
+ const schemaHash = computeSchemaHash({
350
+ msgSchema: msgSchema ?? null,
351
+ stateSchema: stateSchema ?? null,
352
+ msgAnnotations,
353
+ });
354
+ result = injectSchemaHash(result ?? node, schemaHash, f);
314
355
  if (result) {
315
356
  if (hasPos)
316
357
  edits.push({ start: origStart, end: origEnd, replacement: '' });
@@ -326,8 +367,8 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
326
367
  transformed = cleanupImports(transformed, lluiImport, importedHelpers, safeToRemove, usesElSplit, usesElTemplate, usesMemo, usesApplyBinding, usesCloneStaticTemplate, f);
327
368
  if (edits.length === 0)
328
369
  return null;
329
- // Find component declarations for HMR
330
- const componentDecls = devMode ? findComponentDeclarations(sourceFile, lluiImport) : [];
370
+ // Find component declarations for HMR and agent metadata
371
+ const componentDecls = devMode || emitAgentMetadata ? findComponentDeclarations(sourceFile, lluiImport) : [];
331
372
  // Build per-statement edits by comparing original vs transformed.
332
373
  // Only emit edits for statements that actually changed.
333
374
  // Untouched code keeps its original positions → accurate source maps.
@@ -347,7 +388,10 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
347
388
  const { top: _top, bottom: _bottom } = devMode
348
389
  ? generateDevCode(componentDecls, mcpPort)
349
390
  : { top: '', bottom: '' };
350
- const output = (_top ? _top + '\n' : '') + printer.printFile(transformed) + (_bottom ? '\n' + _bottom : '');
391
+ let output = (_top ? _top + '\n' : '') + printer.printFile(transformed) + (_bottom ? '\n' + _bottom : '');
392
+ if (devMode || emitAgentMetadata) {
393
+ output = appendCompilerCacheProps(output, componentDecls);
394
+ }
351
395
  return { output, edits: [{ start: 0, end: source.length, replacement: output }] };
352
396
  }
353
397
  // Compare ignoring trailing semicolons and whitespace (printer adds them)
@@ -379,6 +423,13 @@ export function transformLlui(source, _filename, devMode = false, mcpPort = 5200
379
423
  for (const edit of sorted) {
380
424
  output = output.slice(0, edit.start) + edit.replacement + output.slice(edit.end);
381
425
  }
426
+ // After output is assembled, update postSource in cache and emit non-enumerable props
427
+ if ((devMode || emitAgentMetadata) && componentDecls.length > 0) {
428
+ const cacheProps = appendCompilerCacheProps(output, componentDecls);
429
+ if (cacheProps !== output) {
430
+ output = cacheProps;
431
+ }
432
+ }
382
433
  return { output, edits: finalEdits };
383
434
  }
384
435
  // ── HMR ──────────────────────────────────────────────────────────
@@ -2124,6 +2175,71 @@ function injectMsgSchema(node, schema, f) {
2124
2175
  ...node.arguments.slice(1),
2125
2176
  ]);
2126
2177
  }
2178
+ function hasNonDefaultAnnotation(a) {
2179
+ for (const v of Object.values(a)) {
2180
+ if (v.intent !== null)
2181
+ return true;
2182
+ if (v.alwaysAffordable)
2183
+ return true;
2184
+ if (v.requiresConfirm)
2185
+ return true;
2186
+ if (v.humanOnly)
2187
+ return true;
2188
+ }
2189
+ return false;
2190
+ }
2191
+ function annotationsToObjectLiteral(a) {
2192
+ const props = [];
2193
+ for (const [variant, ann] of Object.entries(a)) {
2194
+ props.push(ts.factory.createPropertyAssignment(variant, ts.factory.createObjectLiteralExpression([
2195
+ ts.factory.createPropertyAssignment('intent', ann.intent === null
2196
+ ? ts.factory.createNull()
2197
+ : ts.factory.createStringLiteral(ann.intent)),
2198
+ ts.factory.createPropertyAssignment('alwaysAffordable', ann.alwaysAffordable ? ts.factory.createTrue() : ts.factory.createFalse()),
2199
+ ts.factory.createPropertyAssignment('requiresConfirm', ann.requiresConfirm ? ts.factory.createTrue() : ts.factory.createFalse()),
2200
+ ts.factory.createPropertyAssignment('humanOnly', ann.humanOnly ? ts.factory.createTrue() : ts.factory.createFalse()),
2201
+ ], true)));
2202
+ }
2203
+ return ts.factory.createObjectLiteralExpression(props, true);
2204
+ }
2205
+ function injectMsgAnnotations(node, annotations, f) {
2206
+ const configArg = node.arguments[0];
2207
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
2208
+ return node;
2209
+ // Don't inject if already present
2210
+ for (const prop of configArg.properties) {
2211
+ if (ts.isPropertyAssignment(prop) &&
2212
+ ts.isIdentifier(prop.name) &&
2213
+ prop.name.text === '__msgAnnotations') {
2214
+ return node;
2215
+ }
2216
+ }
2217
+ const annotationsProp = f.createPropertyAssignment('__msgAnnotations', annotationsToObjectLiteral(annotations));
2218
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, annotationsProp], true);
2219
+ return f.createCallExpression(node.expression, node.typeArguments, [
2220
+ newConfig,
2221
+ ...node.arguments.slice(1),
2222
+ ]);
2223
+ }
2224
+ function injectSchemaHash(node, hash, f) {
2225
+ const configArg = node.arguments[0];
2226
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
2227
+ return node;
2228
+ // Don't inject if already present
2229
+ for (const prop of configArg.properties) {
2230
+ if (ts.isPropertyAssignment(prop) &&
2231
+ ts.isIdentifier(prop.name) &&
2232
+ prop.name.text === '__schemaHash') {
2233
+ return node;
2234
+ }
2235
+ }
2236
+ const hashProp = f.createPropertyAssignment('__schemaHash', f.createStringLiteral(hash));
2237
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, hashProp], true);
2238
+ return f.createCallExpression(node.expression, node.typeArguments, [
2239
+ newConfig,
2240
+ ...node.arguments.slice(1),
2241
+ ]);
2242
+ }
2127
2243
  function injectEffectSchema(node, schema, f) {
2128
2244
  const configArg = node.arguments[0];
2129
2245
  if (!configArg || !ts.isObjectLiteralExpression(configArg))
@@ -2161,6 +2277,29 @@ function injectEffectSchema(node, schema, f) {
2161
2277
  ...node.arguments.slice(1),
2162
2278
  ]);
2163
2279
  }
2280
+ function injectBindingDescriptors(node, descs, f) {
2281
+ const configArg = node.arguments[0];
2282
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
2283
+ return node;
2284
+ // Don't inject if already present
2285
+ for (const prop of configArg.properties) {
2286
+ if (ts.isPropertyAssignment(prop) &&
2287
+ ts.isIdentifier(prop.name) &&
2288
+ prop.name.text === '__bindingDescriptors') {
2289
+ return node;
2290
+ }
2291
+ }
2292
+ const descsProp = f.createPropertyAssignment('__bindingDescriptors', bindingDescriptorsToArrayLiteral(descs));
2293
+ const newConfig = f.createObjectLiteralExpression([...configArg.properties, descsProp], true);
2294
+ return f.createCallExpression(node.expression, node.typeArguments, [
2295
+ newConfig,
2296
+ ...node.arguments.slice(1),
2297
+ ]);
2298
+ }
2299
+ function bindingDescriptorsToArrayLiteral(descs) {
2300
+ const entries = descs.map((d) => ts.factory.createObjectLiteralExpression([ts.factory.createPropertyAssignment('variant', ts.factory.createStringLiteral(d.variant))], false));
2301
+ return ts.factory.createArrayLiteralExpression(entries, true);
2302
+ }
2164
2303
  // ── Per-item accessor detection ──────────────────────────────────
2165
2304
  // ── Item selector deduplication ──────────────────────────────────
2166
2305
  /**
@@ -3293,4 +3432,66 @@ export function hasUseClientDirective(source) {
3293
3432
  }
3294
3433
  return source.startsWith("'use client'", i) || source.startsWith('"use client"', i);
3295
3434
  }
3435
+ // ── Compiler cache helpers ────────────────────────────────────────
3436
+ /**
3437
+ * Extract the view function body (the value of the `view:` property) from
3438
+ * a component() config object literal. Uses a regex heuristic — good enough
3439
+ * for round-tripping source for dev/agent tools.
3440
+ */
3441
+ export function extractViewBody(code) {
3442
+ const match = /\bview\s*:\s*([\s\S]*?)(?=,\s*(?:onEffect|update|init|name|onMsg)\s*:|}\s*\))/m.exec(code);
3443
+ return match?.[1]?.trim() ?? null;
3444
+ }
3445
+ /**
3446
+ * Extract the component `name:` string literal from a component() call's
3447
+ * first argument object literal in the source text.
3448
+ */
3449
+ export function extractComponentNameFromConfig(node) {
3450
+ const configArg = node.arguments[0];
3451
+ if (!configArg || !ts.isObjectLiteralExpression(configArg))
3452
+ return null;
3453
+ for (const prop of configArg.properties) {
3454
+ if (ts.isPropertyAssignment(prop) &&
3455
+ ts.isIdentifier(prop.name) &&
3456
+ prop.name.text === 'name' &&
3457
+ ts.isStringLiteral(prop.initializer)) {
3458
+ return prop.initializer.text;
3459
+ }
3460
+ }
3461
+ return null;
3462
+ }
3463
+ /**
3464
+ * Generate Object.defineProperty calls for __preSource, __postSource,
3465
+ * __msgMaskMap, and __bindingSources on a component variable. These are
3466
+ * non-enumerable so they don't appear in JSON.stringify(componentDef) but are
3467
+ * visible to devtools.
3468
+ */
3469
+ export function generateCompilerCacheProps(varName, componentName) {
3470
+ const entry = compilerCache.get(componentName);
3471
+ if (!entry)
3472
+ return '';
3473
+ return (`\nObject.defineProperty(${varName}, '__preSource', { value: ${JSON.stringify(entry.preSource)}, enumerable: false, configurable: true })` +
3474
+ `\nObject.defineProperty(${varName}, '__postSource', { value: ${JSON.stringify(entry.postSource)}, enumerable: false, configurable: true })` +
3475
+ `\nObject.defineProperty(${varName}, '__msgMaskMap', { value: ${JSON.stringify(entry.msgMaskMap)}, enumerable: false, configurable: true })` +
3476
+ `\nObject.defineProperty(${varName}, '__bindingSources', { value: ${JSON.stringify(entry.bindingSources)}, enumerable: false, configurable: true })`);
3477
+ }
3478
+ /**
3479
+ * After the full output string is assembled, update each cached component's
3480
+ * postSource (extract view body from the transformed output), then append
3481
+ * Object.defineProperty calls for all four compiler-cache properties.
3482
+ */
3483
+ export function appendCompilerCacheProps(output, componentDecls) {
3484
+ let result = output;
3485
+ for (const { varName, componentName } of componentDecls) {
3486
+ const existing = compilerCache.get(componentName);
3487
+ if (!existing)
3488
+ continue;
3489
+ // Update the cache entry with the post-transform view body
3490
+ const postSource = extractViewBody(output) ?? '';
3491
+ compilerCache.set(componentName, { ...existing, postSource });
3492
+ // Append non-enumerable property definitions
3493
+ result += generateCompilerCacheProps(varName, componentName);
3494
+ }
3495
+ return result;
3496
+ }
3296
3497
  //# sourceMappingURL=transform.js.map