@vesk/compiler 0.2.26 → 0.2.28

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.
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA6BnG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAWvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkB/G,QAAQ,IAAI,MAAM;IA0BlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,SAAI,EAAE,KAAK,SAAI,GAAG,MAAM,CAkBrG;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA8BnG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAoB3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAYvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB/G,MAAM,CAAC,cAAc,CAAC,OAAO,GAAE,gBAAgB,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAA;KAAO,GAAG,SAAS;IAmBpF,QAAQ,IAAI,MAAM;IA0BlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
package/dist/errors.js CHANGED
@@ -142,6 +142,7 @@ export class VeskError extends Error {
142
142
  }
143
143
  return new VeskError(msg, {
144
144
  ...context,
145
+ code: context.code || 'V0401',
145
146
  suggestions: [name, ...allCandidates.slice(0, 8)],
146
147
  nextSteps,
147
148
  tip: isBuiltin
@@ -152,6 +153,7 @@ export class VeskError extends Error {
152
153
  static classDecl(context = {}) {
153
154
  return new VeskError('class declarations are not supported inside Vesk components.', {
154
155
  ...context,
156
+ code: context.code || 'V0402',
155
157
  suggestions: [
156
158
  'Use a plain object: const obj = { ... };',
157
159
  'Use a factory function: function create() { return { ... }; }',
@@ -167,6 +169,7 @@ export class VeskError extends Error {
167
169
  static serverBlockInClient(compName, context = {}) {
168
170
  return new VeskError(`{#server} block found in client island "${compName}". Client islands render on both server and client, so {#server} blocks have no effect.`, {
169
171
  ...context,
172
+ code: context.code || 'V0403',
170
173
  suggestions: [
171
174
  `Remove the {#server} block from "${compName}".`,
172
175
  `Or remove the \`client\` keyword from "${compName}" declaration.`,
@@ -181,6 +184,7 @@ export class VeskError extends Error {
181
184
  static clientBlockInServer(compName, context = {}) {
182
185
  return new VeskError(`{#client} block found in component "${compName}", but this component is not a client island. {#client} blocks are only allowed inside components declared with the \`client\` keyword.`, {
183
186
  ...context,
187
+ code: context.code || 'V0404',
184
188
  suggestions: [
185
189
  `Add \`client\`: \`component ${compName} client { ... }\``,
186
190
  `Or remove the {#client}...{/client} block.`,
@@ -201,6 +205,7 @@ export class VeskError extends Error {
201
205
  static configError(msg, validOptions = [], context = {}) {
202
206
  return new VeskError(msg, {
203
207
  ...context,
208
+ code: context.code || 'V0405',
204
209
  suggestions: validOptions.length ? [`Valid options: ${validOptions.join(', ')}`] : [],
205
210
  nextSteps: [
206
211
  'Check your vesk.config file for typos.',
@@ -211,6 +216,7 @@ export class VeskError extends Error {
211
216
  static asyncChildInSyncParent(parentName, childName, context = {}) {
212
217
  return new VeskError(`Component "${parentName}" renders "<${childName} />", but "<${childName} />" is async and "${parentName}" is not declared async.`, {
213
218
  ...context,
219
+ code: context.code || 'V0406',
214
220
  suggestions: [
215
221
  `Declare the parent async: \`async component ${parentName} ...\``,
216
222
  ],
@@ -222,8 +228,23 @@ export class VeskError extends Error {
222
228
  tip: `A component that renders an async component must itself be async so the renderer can await it before serializing the HTML. Async components also include components that call \`useFetch\`.`,
223
229
  });
224
230
  }
231
+ static attrJsxElement(context = {}) {
232
+ return new VeskError(`Attribute "${context.attr ?? ''}" on an HTML element is given a JSX element value. DOM attributes hold plain values — JSX elements belong to content or to a component prop (where they become a content slot).`, {
233
+ ...context,
234
+ code: context.code || 'V0407',
235
+ suggestions: [
236
+ 'Pass the element as a child instead: <div>{ <yourElement /> }</div>',
237
+ 'If the component accepts it, pass the element as a component prop instead of an HTML attribute.',
238
+ ],
239
+ nextSteps: [
240
+ 'Move the JSX element into the element\'s content so it renders as a child node.',
241
+ 'Component props can hold JSX elements — they are threaded as content slots and read back with {props.<name>}.',
242
+ ],
243
+ tip: 'JSX elements are content. They render inside an element or travel as a component prop — never as an attribute value on an HTML tag.',
244
+ });
245
+ }
225
246
  toString() {
226
- let out = `[vesk] ${this.message}`;
247
+ let out = this.code ? `[vesk ${this.code}] ${this.message}` : `[vesk] ${this.message}`;
227
248
  if (this.file) {
228
249
  out += `\n File: ${this.file}`;
229
250
  if (this.line) {
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAoBP,MAAM,uBAAuB,CAAC;AA0L/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAg0BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CA2Q9E"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA0S/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AA07BD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAwN9E"}
@@ -1,12 +1,20 @@
1
- import { IRRoot, ComponentIR, StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, RuntimeStatement, ForLoop, TrackDecl, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, Expression, SlotNode, } from '@vesk/compiler/src/ir';
1
+ import { IRRoot, ComponentIR, StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, RuntimeStatement, ForLoop, TrackDecl, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, Expression, SlotNode, PropSlot, PropSlotRender, } from '@vesk/compiler/src/ir';
2
2
  import { VeskError, codeFrame } from '@vesk/compiler/src/errors';
3
3
  import { createBaseParser } from '@vesk/compiler/src/parser';
4
4
  import { skipWhitespace, findBalancedEnd, splitTopLevel, startsWithIdentifier, stripDeclKeyword, isWhitespaceChar, collapseNewlineWhitespace } from '@vesk/compiler/src/scan';
5
5
  import { stripCodeTypes } from '@vesk/compiler/src/strip-ts';
6
6
  import { stripTypeImport } from '@vesk/compiler/src/vsk-imports';
7
7
  import { importBindingPairs } from '@vesk/compiler/src/module-imports';
8
- import { collectCalledIdentifiers, extractImportNames, importModuleTarget } from '@vesk/compiler/src/tokens';
8
+ import { extractImportNames, importModuleTarget } from '@vesk/compiler/src/tokens';
9
9
  let __vskAnnotations = [];
10
+ /**
11
+ * Prop names of the component currently being processed that are declared as
12
+ * renderable content (`*: Component`) and are therefore threaded through the
13
+ * slot channel. A content read `{props.<name>}` of such a prop compiles to
14
+ * {@link PropSlotRender} instead of a value binding, so the child can render a
15
+ * slot handed to it by a caller that wrote `name={<div/>}`.
16
+ */
17
+ let __slotProps = null;
10
18
  function parseExprNode(text) {
11
19
  try {
12
20
  const ParserClass = createBaseParser();
@@ -39,6 +47,34 @@ function getForClauseAnnotation(forStart) {
39
47
  function getSource(source, node) {
40
48
  return source.slice(node.start, node.end);
41
49
  }
50
+ /**
51
+ * Returns the source text for a loop/map binding pattern, preserving
52
+ * destructuring (`[a, b]`, `{x, y}`, defaults, rest) while stripping a
53
+ * top-level TS type annotation (`[a, b]: T` → `[a, b]`). Plain identifiers
54
+ * return their name directly so `const x: T` still yields `x`.
55
+ */
56
+ function patternSource(source, node) {
57
+ if (!node)
58
+ return 'item';
59
+ if (node.type === 'Identifier')
60
+ return node.name ?? 'item';
61
+ const end = node.typeAnnotation ? node.typeAnnotation.start : node.end;
62
+ const text = source.slice(node.start, end).trim();
63
+ return text || 'item';
64
+ }
65
+ function mapParamSource(source, param) {
66
+ if (!param)
67
+ return 'item';
68
+ if (param.type === 'Identifier')
69
+ return param.name ?? 'item';
70
+ const end = param.typeAnnotation ? param.typeAnnotation.start : param.end;
71
+ if (typeof param.start === 'number' && typeof end === 'number') {
72
+ const text = source.slice(param.start, end).trim();
73
+ if (text)
74
+ return text;
75
+ }
76
+ return 'item';
77
+ }
42
78
  function collectComponentCalls(nodes, out) {
43
79
  for (const n of nodes) {
44
80
  if (n instanceof ComponentCall) {
@@ -48,6 +84,9 @@ function collectComponentCalls(nodes, out) {
48
84
  else if (n instanceof StaticNode || n instanceof ServerBlock || n instanceof ClientBlock || n instanceof HeadBlock) {
49
85
  collectComponentCalls(n.children, out);
50
86
  }
87
+ else if (n instanceof PropSlot) {
88
+ collectComponentCalls(n.body, out);
89
+ }
51
90
  else if (n instanceof MapRegion) {
52
91
  collectComponentCalls(n.bodyTemplate, out);
53
92
  collectComponentCalls(n.alternateNodes, out);
@@ -129,6 +168,92 @@ function estreeCallsFetch(ast) {
129
168
  }
130
169
  return false;
131
170
  }
171
+ /**
172
+ * The simple identifier a member chain *ends* in (`a.b.c` → `c`), or null when
173
+ * the object doesn't end in a bare name token — mirrors the tokenizer's "name
174
+ * two tokens before the call paren" rule so `a.b(` and `a?.b(` surface `a`,
175
+ * `a.b.c(` surfaces `b`, while `this.b(`, `a[0].b(` and `a(x).b(` surface
176
+ * nothing.
177
+ */
178
+ function memberReceiverName(obj) {
179
+ if (!obj || typeof obj !== 'object')
180
+ return null;
181
+ const rec = obj;
182
+ if (rec.type === 'Identifier' && typeof rec.name === 'string')
183
+ return rec.name;
184
+ const prop = rec.type === 'MemberExpression' && !rec.computed ? rec.property : null;
185
+ if (prop && typeof prop === 'object' && prop.type === 'Identifier') {
186
+ const propName = prop.name;
187
+ if (typeof propName === 'string')
188
+ return propName;
189
+ }
190
+ return null;
191
+ }
192
+ /**
193
+ * Single-pass ESTree walk collecting every simple call target in `ast` —
194
+ * identifiers invoked directly (`fn(` / `fn<T>(`, constructor calls included)
195
+ * and the receiver name of a bare member call (`a.b(` → `a`, `a.b?.c(` → `b`)
196
+ * — plus every JSX element tag name. Mirrors the semantics of
197
+ * `collectCalledIdentifiers` over the equivalent text, so one walk of the
198
+ * already-parsed tree replaces re-tokenizing each IR raw fragment.
199
+ */
200
+ function collectCallAndJsxTargets(ast) {
201
+ const targets = new Set();
202
+ const stack = [ast];
203
+ while (stack.length > 0) {
204
+ const candidate = stack.pop();
205
+ if (!candidate || typeof candidate !== 'object')
206
+ continue;
207
+ const node = candidate;
208
+ if (typeof node.type === 'string') {
209
+ if (node.type === 'CallExpression' || node.type === 'NewExpression') {
210
+ const callee = node.callee;
211
+ if (callee && typeof callee === 'object') {
212
+ const c = callee;
213
+ if (c.type === 'Identifier' && typeof c.name === 'string' && node.optional !== true) {
214
+ targets.add(c.name);
215
+ }
216
+ else if (c.type === 'MemberExpression' && !c.computed && node.optional !== true) {
217
+ const receiverName = memberReceiverName(c.object);
218
+ if (receiverName)
219
+ targets.add(receiverName);
220
+ }
221
+ }
222
+ }
223
+ else if (node.type === 'JSXElement') {
224
+ const opening = node.openingElement;
225
+ const tag = opening && typeof opening === 'object' ? opening.name : null;
226
+ if (tag && typeof tag === 'object') {
227
+ const t = tag;
228
+ if (t.type === 'JSXIdentifier' && typeof t.name === 'string') {
229
+ targets.add(t.name);
230
+ }
231
+ else if (t.type === 'JSXMemberExpression') {
232
+ const prop = t.property;
233
+ if (prop && typeof prop === 'object' && prop.type === 'JSXIdentifier') {
234
+ const propName = prop.name;
235
+ if (typeof propName === 'string')
236
+ targets.add(propName);
237
+ }
238
+ }
239
+ }
240
+ }
241
+ for (const key of Object.keys(node)) {
242
+ if (key === 'parent' || key === 'loc' || key === 'start' || key === 'end' || key === 'range')
243
+ continue;
244
+ const value = node[key];
245
+ if (Array.isArray(value)) {
246
+ for (const item of value)
247
+ stack.push(item);
248
+ }
249
+ else {
250
+ stack.push(value);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ return targets;
256
+ }
132
257
  function componentUsesFetch(nodes) {
133
258
  for (const node of nodes) {
134
259
  if (node instanceof ServerBlock || node instanceof ClientBlock) {
@@ -281,10 +406,99 @@ function processAttribute(source, attr) {
281
406
  const expr = attr.value.expression;
282
407
  if (expr.type === 'Literal')
283
408
  return { name, value: String(expr.value) };
409
+ // A JSX element can never be a DOM attribute value — elements belong to
410
+ // content/slots, not to an attribute slot on an HTML tag. Surfacing a real
411
+ // error here beats a raw `(<div/>)` leaking into compiled JS (the previous
412
+ // behavior crashed with a runtime SyntaxError deep inside codegen).
413
+ if (containsJSX(expr)) {
414
+ throw VeskError.attrJsxElement({ attr: name });
415
+ }
284
416
  return { name, value: toExpression(source, expr) };
285
417
  }
286
418
  return { name, value: '' };
287
419
  }
420
+ /**
421
+ * True when `node` (an expression AST) contains a JSX element or fragment
422
+ * anywhere in its subtree — used to distinguish slot-bearing prop values
423
+ * (`trigger={<Button/>}`, `trigger={open ? <A/> : <B/>}`) from plain ones
424
+ * (`align={'end'}`, `count={n + 1}`).
425
+ */
426
+ function containsJSX(node) {
427
+ if (!node || typeof node !== 'object')
428
+ return false;
429
+ if (node.type === 'JSXElement' || node.type === 'JSXFragment')
430
+ return true;
431
+ if (node.type === 'JSXExpressionContainer')
432
+ return containsJSX(node.expression);
433
+ // TS-only position holders (type annotations, parameter names) never contain
434
+ // JSX — skipping them avoids walking TS structure.
435
+ for (const key of [
436
+ 'expression', 'object', 'property', 'callee', 'arguments', 'test',
437
+ 'consequent', 'alternate', 'left', 'right', 'init', 'elements', 'params', 'body',
438
+ ]) {
439
+ const child = node[key];
440
+ if (Array.isArray(child)) {
441
+ for (const c of child)
442
+ if (containsJSX(c))
443
+ return true;
444
+ }
445
+ else if (child && containsJSX(child)) {
446
+ return true;
447
+ }
448
+ }
449
+ return false;
450
+ }
451
+ /**
452
+ * Returns the prop name when `expr` is a bare `props.<name>` read, else null.
453
+ */
454
+ function slotReadPropName(expr) {
455
+ if (expr.type === 'MemberExpression' && !expr.computed &&
456
+ expr.object.type === 'Identifier' && expr.object.name === 'props' &&
457
+ expr.property.type === 'Identifier') {
458
+ return expr.property.name;
459
+ }
460
+ return null;
461
+ }
462
+ /**
463
+ * Parses an inline component props type (`{ trigger: Component; ... }`) and
464
+ * returns the set of prop names declared as renderable content — any member
465
+ * whose type text mentions `Component`. Those props may be handed a JSX
466
+ * element at the call site and are read back with `{props.<name>}` as slots.
467
+ */
468
+ function slotPropNamesFromType(propsType) {
469
+ const out = new Set();
470
+ if (!propsType)
471
+ return out;
472
+ const trimmed = propsType.trim();
473
+ if (trimmed.startsWith('{')) {
474
+ const inner = findBalancedEnd(trimmed, 0) === trimmed.length - 1
475
+ ? trimmed.slice(1, -1)
476
+ : trimmed;
477
+ for (const part of splitTopLevel(inner, ';')) {
478
+ const colon = findTopLevelColon(part);
479
+ if (colon === -1)
480
+ continue;
481
+ const namePart = part.slice(0, colon).trim().replace(/^\$/, '').replace(/\?$/, '').trim();
482
+ const typePart = part.slice(colon + 1).trim();
483
+ if (namePart && typePart.includes('Component'))
484
+ out.add(namePart);
485
+ }
486
+ }
487
+ return out;
488
+ }
489
+ function findTopLevelColon(text) {
490
+ let depth = 0;
491
+ for (let i = 0; i < text.length; i++) {
492
+ const ch = text[i];
493
+ if (ch === '(' || ch === '[' || ch === '{' || ch === '<')
494
+ depth++;
495
+ else if (ch === ')' || ch === ']' || ch === '}')
496
+ depth = Math.max(0, depth - 1);
497
+ else if (ch === ':' && depth === 0)
498
+ return i;
499
+ }
500
+ return -1;
501
+ }
288
502
  /**
289
503
  * Recognizes a statement-mode `if (cond)` opening among JSX children.
290
504
  * Returns the condition source, or null when `text` does not open an if
@@ -433,7 +647,7 @@ function processJSXChildren(source, children) {
433
647
  const exprText = ofParts[1].trim();
434
648
  const arrExpr = new Expression(exprText, [], parseExprNode(exprText), null);
435
649
  const rawFor = hasForPrefix(child.value);
436
- const ann = rawFor ? getForClauseAnnotation(child.start + rawFor) : null;
650
+ const ann = rawFor !== -1 ? getForClauseAnnotation(child.start + rawFor) : null;
437
651
  const keyExpr = ann?.keyRange ? new Expression(source.slice(ann.keyRange[0], ann.keyRange[1])) : null;
438
652
  const indexVar = ann?.indexName ?? null;
439
653
  let alternate = [];
@@ -470,7 +684,7 @@ function processJSXChildren(source, children) {
470
684
  }
471
685
  if (isMapCall(expr)) {
472
686
  const arrowFn = expr.arguments[0];
473
- const itemVar = arrowFn.params[0]?.name ?? 'item';
687
+ const itemVar = mapParamSource(source, arrowFn.params[0]);
474
688
  const indexVar = arrowFn.params[1]?.name ?? null;
475
689
  const bodyNodes = processJSXCallbackBody(source, arrowFn.body);
476
690
  const arrayExpr = toExpression(source, expr.callee.object);
@@ -502,6 +716,16 @@ function processJSXChildren(source, children) {
502
716
  i++;
503
717
  continue;
504
718
  }
719
+ // A content read of a component-declared slot prop (`{props.trigger}`)
720
+ // must render the threaded content — not stringify a fragment.
721
+ {
722
+ const slotName = slotReadPropName(expr);
723
+ if (slotName !== null && __slotProps !== null && __slotProps.has(slotName)) {
724
+ result.push(new PropSlotRender(slotName));
725
+ i++;
726
+ continue;
727
+ }
728
+ }
505
729
  result.push(new DynamicBinding(toExpression(source, expr)));
506
730
  i++;
507
731
  }
@@ -557,7 +781,7 @@ function exprToIR(source, expr) {
557
781
  }
558
782
  if (isMapCall(expr)) {
559
783
  const arrowFn = expr.arguments[0];
560
- const itemVar = arrowFn.params[0]?.name ?? 'item';
784
+ const itemVar = mapParamSource(source, arrowFn.params[0]);
561
785
  const indexVar = arrowFn.params[1]?.name ?? null;
562
786
  const bodyNodes = processJSXCallbackBody(source, arrowFn.body);
563
787
  const arrayExpr = toExpression(source, expr.callee.object);
@@ -580,6 +804,19 @@ function exprToIR(source, expr) {
580
804
  const consequent = exprToIR(source, expr.right);
581
805
  return [new OpaqueDynamicRegion(condExpr, consequent)];
582
806
  }
807
+ // `props.children` (or bare `children`) inside an expression branch must
808
+ // insert the slot nodes — not degrade to a text binding that stringifies
809
+ // the fragment (`String(props.children)` → "[object DocumentFragment]").
810
+ if ((expr.type === 'MemberExpression' && !expr.computed &&
811
+ expr.object.type === 'Identifier' && expr.object.name === 'props' &&
812
+ expr.property.type === 'Identifier' && expr.property.name === 'children')
813
+ || (expr.type === 'Identifier' && expr.name === 'children')) {
814
+ return [new SlotNode()];
815
+ }
816
+ const slotName = slotReadPropName(expr);
817
+ if (slotName !== null && __slotProps !== null && __slotProps.has(slotName)) {
818
+ return [new PropSlotRender(slotName)];
819
+ }
583
820
  return [new DynamicBinding(toExpression(source, expr))];
584
821
  }
585
822
  /**
@@ -633,18 +870,18 @@ function processJSXElement(source, element) {
633
870
  // actual in-scope value instead of `document.createElement("it.icon")` or a
634
871
  // registry lookup by dotted string.
635
872
  if (nameNode && nameNode.type === 'JSXMemberExpression') {
636
- const { props, spreadProps } = extractProps(source, element);
873
+ const { props, spreadProps, slots } = extractProps(source, element);
637
874
  const children = selfClosing ? [] : processJSXChildren(source, element.children || []);
638
- return [new ComponentCall(tagName, props, children, spreadProps, element.start, getSource(source, nameNode))];
875
+ return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start, getSource(source, nameNode))];
639
876
  }
640
877
  if (!isHTMLTag(tagName) && selfClosing) {
641
- const { props, spreadProps } = extractProps(source, element);
642
- return [new ComponentCall(tagName, props, [], spreadProps, element.start)];
878
+ const { props, spreadProps, slots } = extractProps(source, element);
879
+ return [new ComponentCall(tagName, props, slots.map((s) => new PropSlot(s.name, s.nodes)), spreadProps, element.start)];
643
880
  }
644
881
  if (!isHTMLTag(tagName)) {
645
- const { props, spreadProps } = extractProps(source, element);
882
+ const { props, spreadProps, slots } = extractProps(source, element);
646
883
  const children = processJSXChildren(source, element.children || []);
647
- return [new ComponentCall(tagName, props, children, spreadProps, element.start)];
884
+ return [new ComponentCall(tagName, props, [...children, ...slots.map((s) => new PropSlot(s.name, s.nodes))], spreadProps, element.start)];
648
885
  }
649
886
  const attributes = element.openingElement.attributes
650
887
  .filter((attr) => attr.type !== 'JSXSpreadAttribute')
@@ -677,22 +914,34 @@ function processJSXElement(source, element) {
677
914
  function extractProps(source, element) {
678
915
  const props = [];
679
916
  const spreadProps = [];
917
+ const slots = [];
680
918
  for (const attr of element.openingElement.attributes) {
681
919
  if (attr.type === 'JSXSpreadAttribute') {
682
920
  spreadProps.push(toExpression(source, attr.argument));
683
921
  }
684
922
  else {
685
- props.push({
686
- name: attr.name.type === 'JSXIdentifier' ? attr.name.name : getSource(source, attr.name),
687
- value: attr.value === null
688
- ? new Expression('true')
689
- : attr.value.type === 'JSXExpressionContainer'
690
- ? toExpression(source, attr.value.expression)
691
- : new Expression(JSON.stringify(attr.value.value)),
692
- });
923
+ const name = attr.name.type === 'JSXIdentifier' ? attr.name.name : getSource(source, attr.name);
924
+ let value;
925
+ if (attr.value === null) {
926
+ value = new Expression('true');
927
+ }
928
+ else if (attr.value.type === 'JSXExpressionContainer') {
929
+ const expr = attr.value.expression;
930
+ // JSX elements as prop values are named content slots (`trigger={<Button/>}`),
931
+ // not scalar values — the call site hoists them into the slot channel.
932
+ if (containsJSX(expr)) {
933
+ slots.push({ name, nodes: exprToIR(source, expr) });
934
+ continue;
935
+ }
936
+ value = toExpression(source, expr);
937
+ }
938
+ else {
939
+ value = new Expression(JSON.stringify(attr.value.value));
940
+ }
941
+ props.push({ name, value });
693
942
  }
694
943
  }
695
- return { props, spreadProps };
944
+ return { props, spreadProps, slots };
696
945
  }
697
946
  function buildGuardChain(source, guardClauses, mainReturn) {
698
947
  const mainBody = [];
@@ -842,9 +1091,16 @@ function processIfStatement(source, stmt) {
842
1091
  function processForStatement(source, stmt, alternate = []) {
843
1092
  if (stmt.type === 'ForOfStatement') {
844
1093
  const left = stmt.left;
845
- const itemVar = left.type === 'VariableDeclaration'
846
- ? left.declarations[0]?.id?.name ?? 'item'
847
- : left.name ?? 'item';
1094
+ let itemVar = 'item';
1095
+ if (left.type === 'VariableDeclaration') {
1096
+ itemVar = patternSource(source, left.declarations[0]?.id);
1097
+ }
1098
+ else if (left.type === 'Identifier') {
1099
+ itemVar = left.name ?? 'item';
1100
+ }
1101
+ else if (typeof left.start === 'number' && typeof left.end === 'number') {
1102
+ itemVar = source.slice(left.start, left.end).trim() || 'item';
1103
+ }
848
1104
  const arrayExpr = toExpression(source, stmt.right);
849
1105
  const bodyTemplate = processBlockBody(source, stmt.body);
850
1106
  const ann = getForClauseAnnotation(stmt.start);
@@ -1112,6 +1368,7 @@ function idxOfQuote(s, from) {
1112
1368
  }
1113
1369
  export function generateIR(ast, source, filename) {
1114
1370
  __vskAnnotations = ast.__vskAnnotations ?? [];
1371
+ __slotProps = null;
1115
1372
  const file = filename || '';
1116
1373
  const components = [];
1117
1374
  const imports = [];
@@ -1206,6 +1463,11 @@ export function generateIR(ast, source, filename) {
1206
1463
  const propsType = getPropsType(inner.params, source);
1207
1464
  const bodyStmts = inner.body.body;
1208
1465
  const isClientComp = !!inner.client;
1466
+ // Slot props are declared at the component boundary (`trigger: Component`),
1467
+ // so content reads of `props.<name>` compile to slot renders instead of
1468
+ // value bindings for THIS component only.
1469
+ const prevSlotProps = __slotProps;
1470
+ __slotProps = slotPropNamesFromType(propsType);
1209
1471
  if (isStatementMode(bodyStmts)) {
1210
1472
  const raw = processStatementModeBody(source, bodyStmts, file);
1211
1473
  const { body, css } = extractStyle(raw);
@@ -1213,6 +1475,7 @@ export function generateIR(ast, source, filename) {
1213
1475
  const comp = new ComponentIR(name, paramNames, body, { mode: 'statement', exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType });
1214
1476
  comp.style = css;
1215
1477
  components.push(comp);
1478
+ __slotProps = prevSlotProps;
1216
1479
  }
1217
1480
  else {
1218
1481
  const guardClauses = [];
@@ -1261,6 +1524,7 @@ export function generateIR(ast, source, filename) {
1261
1524
  const comp = new ComponentIR(name, paramNames, body, { exported, defaultExport, isClient: inner.client, isAsync: inner.async, ssrAwait: componentUsesFetch(body), propsType });
1262
1525
  comp.style = css;
1263
1526
  components.push(comp);
1527
+ __slotProps = prevSlotProps;
1264
1528
  }
1265
1529
  }
1266
1530
  const asyncNames = new Set(components.filter((c) => c.isAsync || c.ssrAwait).map((c) => c.name));
@@ -1292,83 +1556,17 @@ export function generateIR(ast, source, filename) {
1292
1556
  'createResource', 'getAction', 'validateActionInput', 'issuesToFieldMap', 'isFormAction',
1293
1557
  'Show', 'For', 'Switch', 'Match',
1294
1558
  ];
1559
+ // One walk of the already-parsed tree finds every auto-importable call
1560
+ // target and JSX element name. Every IR raw the old per-fragment scan
1561
+ // re-tokenized ("addUsedFrom" over ~15 text slices) is a subtree of this
1562
+ // very same `ast` — nothing is lost, but the acorn tokenizer is invoked once
1563
+ // per file instead of once per fragment.
1564
+ const called = collectCallAndJsxTargets(ast);
1295
1565
  const usedFunctions = new Set();
1296
- const addUsedFrom = (code) => {
1297
- const called = collectCalledIdentifiers(code);
1298
- for (const fn of autoImportable) {
1299
- if (called.has(fn))
1300
- usedFunctions.add(fn);
1301
- }
1302
- };
1303
- for (const code of topLevelCode)
1304
- addUsedFrom(code);
1305
- for (const code of [loadFn, staticProps]) {
1306
- if (code)
1307
- addUsedFrom(code);
1308
- }
1309
- function scanForAutoImport(nodes) {
1310
- for (const node of nodes) {
1311
- if (node instanceof RuntimeStatement && node.raw) {
1312
- addUsedFrom(node.raw);
1313
- }
1314
- if (node instanceof TrackDecl && node.init) {
1315
- addUsedFrom(node.init);
1316
- }
1317
- if (node instanceof ForLoop) {
1318
- if (node.init)
1319
- addUsedFrom(node.init);
1320
- if (node.update)
1321
- addUsedFrom(node.update);
1322
- if (node.condition && node.condition.raw)
1323
- addUsedFrom(node.condition.raw);
1324
- scanForAutoImport(node.bodyTemplate);
1325
- }
1326
- if (node instanceof WhileLoop) {
1327
- if (node.condition && node.condition.raw)
1328
- addUsedFrom(node.condition.raw);
1329
- scanForAutoImport(node.bodyTemplate);
1330
- }
1331
- if (node instanceof SwitchBlock) {
1332
- if (node.discriminant && node.discriminant.raw)
1333
- addUsedFrom(node.discriminant.raw);
1334
- for (const c of node.cases) {
1335
- if (c.test && c.test.raw)
1336
- addUsedFrom(c.test.raw);
1337
- scanForAutoImport(c.body);
1338
- }
1339
- }
1340
- if (node instanceof TryCatch) {
1341
- scanForAutoImport(node.bodyTemplate);
1342
- scanForAutoImport(node.catchBody);
1343
- }
1344
- if (node instanceof ComponentCall) {
1345
- if (autoImportable.includes(node.componentName)) {
1346
- usedFunctions.add(node.componentName);
1347
- }
1348
- for (const prop of node.props) {
1349
- if (prop.value && prop.value.raw) {
1350
- addUsedFrom(prop.value.raw);
1351
- }
1352
- }
1353
- scanForAutoImport(node.children);
1354
- }
1355
- if (node instanceof MapRegion) {
1356
- scanForAutoImport(node.bodyTemplate);
1357
- scanForAutoImport(node.alternateNodes);
1358
- }
1359
- if (node instanceof OpaqueDynamicRegion) {
1360
- scanForAutoImport(node.consequentNodes);
1361
- scanForAutoImport(node.alternateNodes);
1362
- }
1363
- if (node instanceof DynamicBinding && node.expression && node.expression.raw) {
1364
- addUsedFrom(node.expression.raw);
1365
- }
1366
- if (node instanceof StaticNode || node instanceof ServerBlock || node instanceof ClientBlock || node instanceof HeadBlock) {
1367
- scanForAutoImport(node.children);
1368
- }
1369
- }
1566
+ for (const fn of autoImportable) {
1567
+ if (called.has(fn))
1568
+ usedFunctions.add(fn);
1370
1569
  }
1371
- scanForAutoImport(components.flatMap(c => c.body));
1372
1570
  if (usedFunctions.size > 0) {
1373
1571
  const existing = new Set();
1374
1572
  for (const imp of imports) {
package/dist/ir.d.ts CHANGED
@@ -104,6 +104,29 @@ export declare class ComponentCall {
104
104
  export declare class SlotNode {
105
105
  constructor();
106
106
  }
107
+ /**
108
+ * A named content slot threaded to a component through a prop that was given a
109
+ * JSX element value at the call site (`<Popover trigger={<Button/>}>`). The
110
+ * prop is neither a scalar value nor JSON-able — it is renderable content, so
111
+ * codegen hoists the slot body and passes it under the prop name using the
112
+ * exact same channel as `children` (a serialized string IIFE on the server, a
113
+ * fresh DocumentFragment on the client). Child components draw it back with
114
+ * `{props.<name>}`, which compiles to a {@link PropSlotRender}.
115
+ */
116
+ export declare class PropSlot {
117
+ propName: string;
118
+ body: IRNode[];
119
+ constructor(propName: string, body: IRNode[]);
120
+ }
121
+ /**
122
+ * A content read of a slot prop (`{props.trigger}`). The runtime value behind
123
+ * it is threaded through the same channel as `children`, so emitting it on the
124
+ * server pushes the serialized string and on the client appends the fragment.
125
+ */
126
+ export declare class PropSlotRender {
127
+ propName: string;
128
+ constructor(propName: string);
129
+ }
107
130
  export declare class WhileLoop {
108
131
  condition: Expression;
109
132
  bodyTemplate: IRNode[];
@@ -163,5 +186,5 @@ export declare class HeadBlock {
163
186
  children: IRNode[];
164
187
  constructor(children: IRNode[]);
165
188
  }
166
- export type IRNode = StaticNode | TextNode | DynamicBinding | OpaqueDynamicRegion | MapRegion | WhileLoop | SwitchBlock | TryCatch | ForLoop | TrackDecl | RuntimeStatement | ComponentRef | ComponentCall | ServerBlock | ClientBlock | HeadBlock | SlotNode;
189
+ export type IRNode = StaticNode | TextNode | DynamicBinding | OpaqueDynamicRegion | MapRegion | WhileLoop | SwitchBlock | TryCatch | ForLoop | TrackDecl | RuntimeStatement | ComponentRef | ComponentCall | ServerBlock | ClientBlock | HeadBlock | SlotNode | PropSlot | PropSlotRender;
167
190
  //# sourceMappingURL=ir.d.ts.map