@kudzujs/core 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -264,9 +264,32 @@ const rows = items.map(item =>
264
264
  return <ul>{rows}</ul>
265
265
  ```
266
266
 
267
- Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
267
+ The root may also be a top-level same-file row component. Kudzu specializes each call at build time, so projected props, callback props, and simple local calculations compile to the same intrinsic list template:
268
268
 
269
- Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root, and `key={item.<field>}`. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, locals, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Nested item conditions or lists, item spreads, component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
269
+ ```tsx
270
+ function ItemRow({ name, done, onRemove }: {
271
+ name: string
272
+ done: boolean
273
+ onRemove: () => void
274
+ }) {
275
+ const className = done ? "done" : "active"
276
+ return <li className={className}>
277
+ {name}
278
+ <button onClick={() => onRemove()}>Remove</button>
279
+ </li>
280
+ }
281
+
282
+ const rows = items.map(item => <ItemRow
283
+ key={item.id}
284
+ name={item.name}
285
+ done={item.done}
286
+ onRemove={() => setItems(items.filter(entry => entry.id !== item.id))}
287
+ />)
288
+ ```
289
+
290
+ The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers use direct DOM listeners and receive the latest JSON-safe item for their key, including after updates, additions, and reorders. The item remains stored once in shared list state; handler descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
291
+
292
+ Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level same-file row component, and `key={item.<field>}`. Row components accept destructured projected props and top-level single-`const` calculations before one intrinsic return. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Exported or imported row components, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
270
293
 
271
294
  ## Normal JavaScript
272
295
 
@@ -438,6 +461,8 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
438
461
  | Svelte CSR | No | 12.9 KB | 33.1 KB | 828 ms | 5.8 ms | 38.9 ms | 4.0 ms | 5.9 ms | 54.6 ms |
439
462
  | Qwik CSR | No | 22.2 KB | 64.1 KB | 594 ms | 9.1 ms | 22.2 ms | 30.8 ms | 19.0 ms | 81.1 ms |
440
463
 
464
+ An intrinsic-root versus projected-prop row-component A/B build produced byte-for-byte identical `dist` output: 5,175 B JS gzip and 61,731 B total. Seven interleaved clean builds measured 467 ms and 455 ms. Browser operation medians totaled 23.7 ms and 23.9 ms respectively; because the deployed HTML and JavaScript are identical, the 0.2 ms difference is measurement variance rather than component runtime overhead.
465
+
441
466
  Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 23.2 ms, 10.7 ms behind the hand-authored Astro baseline and 7.1 ms ahead of React across all four operations.
442
467
 
443
468
  Benchmark snapshot collected on July 22, 2026 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
@@ -395,6 +395,7 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
395
395
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
396
396
  const settersByFunction = new Map()
397
397
  const functions = new Map()
398
+ const components = new Map()
398
399
  const jsxLocalDeclarations = new Map()
399
400
  const jsxLocalsByFunction = new Map()
400
401
  const listLocalDeclarations = new WeakSet()
@@ -420,9 +421,13 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
420
421
  }
421
422
  }
422
423
  }
423
- if (ts.isFunctionDeclaration(node) && node.name) functions.set(node.name.text, node)
424
+ if (ts.isFunctionDeclaration(node) && node.name) {
425
+ functions.set(node.name.text, node)
426
+ if (node.parent === sourceFile) components.set(node.name.text, { function: node, declaration: node })
427
+ }
424
428
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
425
429
  functions.set(node.name.text, node.initializer)
430
+ if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
426
431
  }
427
432
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isTopLevelConst(node)) {
428
433
  const owner = nearestFunction(node)
@@ -466,8 +471,66 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
466
471
  if (uses.length) listLocalUses.set(uses[0], parts)
467
472
  }
468
473
  }
474
+ const rawRenderedLists = []
475
+ const collectRenderedLists = node => {
476
+ if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
477
+ const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction))
478
+ if (parts) rawRenderedLists.push({ node, parts })
479
+ }
480
+ ts.forEachChild(node, collectRenderedLists)
481
+ }
482
+ collectRenderedLists(sourceFile)
483
+ const fail = (node, message) => {
484
+ const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
485
+ throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
486
+ }
487
+ const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
488
+ const tag = jsxTagName(parts.root)
489
+ return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
490
+ }))
491
+ const componentSpecializations = new WeakMap()
492
+ const specializedDeclarations = new WeakSet()
493
+ for (const name of listComponentNames) {
494
+ const component = components.get(name)
495
+ if (!component) fail(sourceFile, `Keyed list component ${name} must be declared at the top level in the same file`)
496
+ if (isExportedDeclaration(component.declaration)) fail(component.declaration, `Keyed list component ${name} cannot be exported`)
497
+ const calls = jsxTagUses(sourceFile, name)
498
+ if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `Keyed list component ${name} may only be referenced as JSX`)
499
+ for (const call of calls) componentSpecializations.set(call, specializeComponentCall(call, component.function, sourceFile, factory, context, fail))
500
+ specializedDeclarations.add(component.declaration)
501
+ }
502
+ const renderedLists = new WeakMap()
503
+ for (const { node, parts: originalParts } of rawRenderedLists) {
504
+ if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
505
+ const specialization = componentSpecializations.get(originalParts.root)
506
+ const root = specialization?.root ?? originalParts.root
507
+ const callback = root === originalParts.root ? originalParts.callback : factory.updateArrowFunction(
508
+ originalParts.callback,
509
+ originalParts.callback.modifiers,
510
+ originalParts.callback.typeParameters,
511
+ originalParts.callback.parameters,
512
+ originalParts.callback.type,
513
+ originalParts.callback.equalsGreaterThanToken,
514
+ root
515
+ )
516
+ if (callback !== originalParts.callback) {
517
+ ts.setParentRecursive(callback, false)
518
+ callback.parent = originalParts.callback.parent
519
+ }
520
+ const parts = { ...originalParts, root, callback }
521
+ for (const calculation of specialization?.calculations ?? []) {
522
+ ts.setParentRecursive(calculation, false)
523
+ calculation.parent = callback
524
+ validateListExpression(calculation, parts.item, originalParts.root, fail)
525
+ }
526
+ validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions)
527
+ renderedLists.set(node, parts)
528
+ }
469
529
 
470
530
  const visitor = node => {
531
+ if (specializedDeclarations.has(node)) return node
532
+ if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
533
+
471
534
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
472
535
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(modulePath(node.moduleSpecifier.text)), node.attributes)
473
536
  }
@@ -523,10 +586,8 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
523
586
  }
524
587
 
525
588
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
526
- const listParts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction))
589
+ const listParts = renderedLists.get(node)
527
590
  if (listParts) {
528
- if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
529
- validateKeyedList(listParts, sourceFile, settersForNode(node, settersByFunction), listValues, listEventItems, listConditions)
530
591
  usesBehavior = true
531
592
  usesList = true
532
593
  return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, [
@@ -628,11 +689,13 @@ function keyedListParts(expression, setters) {
628
689
  return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
629
690
  }
630
691
 
631
- function validateKeyedList(parts, sourceFile, setters, listValues, listEventItems, listConditions) {
692
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
632
693
  const fail = (node, message) => {
633
694
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
634
695
  throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
635
696
  }
697
+ const root = parts.root
698
+ const item = parts.item
636
699
  const validateElement = node => {
637
700
  const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
638
701
  if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
@@ -641,10 +704,10 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
641
704
  const visit = node => {
642
705
  if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
643
706
  if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
644
- if (node !== parts.root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed lists are not supported")
645
- if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, parts.item)) fail(node, "Keyed list item spreads are not supported")
707
+ if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, "Nested keyed lists are not supported")
708
+ if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
646
709
  if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.getText())) {
647
- listEventItems.set(node, parts.item)
710
+ listEventItems.set(node, item)
648
711
  return
649
712
  }
650
713
  if (ts.isJsxExpression(node) && node.expression) {
@@ -652,16 +715,16 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
652
715
  const condition = conditionalParts(expression)
653
716
  if (condition && containsJsx(expression)) {
654
717
  if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
655
- if (!referencesIdentifier(condition.condition, parts.item)) fail(node, "Keyed list item conditions must read the item")
656
- validateListExpression(condition.condition, parts.item, node, fail)
657
- listConditions.set(node.expression, { ...condition, item: parts.item })
718
+ if (!referencesIdentifier(condition.condition, item)) fail(node, "Keyed list item conditions must read the item")
719
+ validateListExpression(condition.condition, item, node, fail)
720
+ listConditions.set(node.expression, { ...condition, item })
658
721
  conditionDepth++
659
722
  visit(condition.truthy)
660
723
  visit(condition.falsy)
661
724
  conditionDepth--
662
725
  return
663
726
  }
664
- const field = directProperty(expression, parts.item)
727
+ const field = directProperty(expression, item)
665
728
  const isRootKey = ts.isJsxAttribute(node.parent) && node.parent.name.getText() === "key"
666
729
  if (field && ["__proto__", "constructor", "prototype"].includes(field)) fail(node, `Keyed list item property "${field}" is not supported`)
667
730
  if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
@@ -670,16 +733,143 @@ function validateKeyedList(parts, sourceFile, setters, listValues, listEventItem
670
733
  listValues.set(node.expression, { field })
671
734
  return
672
735
  }
673
- if (referencesIdentifier(expression, parts.item)) {
674
- validateListExpression(expression, parts.item, node, fail)
736
+ if (referencesIdentifier(expression, item)) {
737
+ validateListExpression(expression, item, node, fail)
675
738
  if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.getText().toLowerCase())) fail(node, `Keyed list item ${node.parent.name.getText()} is not supported`)
676
- listValues.set(node.expression, { item: parts.item })
739
+ listValues.set(node.expression, { item })
677
740
  return
678
741
  }
679
742
  }
680
743
  ts.forEachChild(node, visit)
681
744
  }
682
- visit(parts.root)
745
+ visit(root)
746
+ }
747
+
748
+ function specializeComponentCall(call, component, sourceFile, factory, context, fail) {
749
+ if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, "Keyed list components must be synchronous")
750
+ if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Keyed list components must use one destructured props parameter")
751
+ if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, "Keyed list component children are not supported")
752
+ const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
753
+ const props = new Map()
754
+ let key
755
+ for (const attribute of callAttributes.properties) {
756
+ if (ts.isJsxSpreadAttribute(attribute)) fail(attribute, "Keyed list component prop spreads are not supported")
757
+ const name = attribute.name.getText()
758
+ if (props.has(name) || name === "key" && key) fail(attribute, `Duplicate keyed list component prop "${name}"`)
759
+ const value = !attribute.initializer
760
+ ? factory.createTrue()
761
+ : ts.isStringLiteral(attribute.initializer)
762
+ ? factory.createStringLiteral(attribute.initializer.text)
763
+ : ts.isJsxExpression(attribute.initializer) && attribute.initializer.expression
764
+ ? attribute.initializer.expression
765
+ : factory.createIdentifier("undefined")
766
+ if (name === "key") key = attribute
767
+ else props.set(name, value)
768
+ }
769
+ const substitutions = new Map()
770
+ const acceptedProps = new Set()
771
+ for (const element of component.parameters[0].name.elements) {
772
+ if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) fail(element, "Keyed list component props cannot use rest, defaults, or nested destructuring")
773
+ const prop = (element.propertyName ?? element.name).getText()
774
+ acceptedProps.add(prop)
775
+ substitutions.set(element.name.text, props.get(prop) ?? factory.createIdentifier("undefined"))
776
+ }
777
+ for (const prop of props.keys()) if (!acceptedProps.has(prop)) fail(call, `Unknown keyed list component prop "${prop}"`)
778
+
779
+ let returned
780
+ const calculations = []
781
+ if (!ts.isBlock(component.body)) {
782
+ returned = component.body
783
+ } else {
784
+ const statements = [...component.body.statements]
785
+ const last = statements.pop()
786
+ if (!last || !ts.isReturnStatement(last) || !last.expression) fail(component.body, "Keyed list component must end with one JSX return")
787
+ for (const statement of statements) {
788
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, "Keyed list component locals must be single const declarations")
789
+ const declaration = statement.declarationList.declarations[0]
790
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Keyed list component locals must be initialized identifiers")
791
+ const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
792
+ calculations.push(calculation)
793
+ substitutions.set(declaration.name.text, calculation)
794
+ }
795
+ returned = last.expression
796
+ }
797
+ let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
798
+ if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, "Keyed list component must return one JSX element")
799
+ const tag = jsxTagName(root)
800
+ if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, "Keyed list component must directly return an intrinsic JSX element")
801
+ const rootAttributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
802
+ if (rootAttributes.properties.some(attribute => ts.isJsxAttribute(attribute) && attribute.name.text === "key")) fail(root, "Keyed list component intrinsic root cannot declare key")
803
+ if (key) root = addJsxAttribute(root, cloneAst(key, factory, context), factory)
804
+ ts.setParentRecursive(root, false)
805
+ root.parent = call.parent
806
+ return { root, calculations }
807
+ }
808
+
809
+ function substituteClone(root, substitutions, factory, context) {
810
+ const visit = (node, shadowed = new Set()) => {
811
+ if (ts.isShorthandPropertyAssignment(node) && substitutions.has(node.name.text) && !shadowed.has(node.name.text)) {
812
+ return factory.createPropertyAssignment(cloneAst(node.name, factory, context), cloneAst(substitutions.get(node.name.text), factory, context))
813
+ }
814
+ if (ts.isIdentifier(node) && substitutions.has(node.text) && !shadowed.has(node.text) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node)) {
815
+ return cloneAst(substitutions.get(node.text), factory, context)
816
+ }
817
+ const nextShadowed = isFunctionLike(node)
818
+ ? new Set([...shadowed, ...node.parameters.flatMap(parameter => bindingNames(parameter.name))])
819
+ : shadowed
820
+ const clone = factory.cloneNode(node)
821
+ ts.setTextRange(clone, node)
822
+ ts.setOriginalNode(clone, node)
823
+ return ts.visitEachChild(clone, child => visit(child, nextShadowed), context)
824
+ }
825
+ return visit(root)
826
+ }
827
+
828
+ function cloneAst(root, factory, context) {
829
+ const visit = node => {
830
+ const clone = factory.cloneNode(node)
831
+ ts.setTextRange(clone, node)
832
+ ts.setOriginalNode(clone, node)
833
+ return ts.visitEachChild(clone, visit, context)
834
+ }
835
+ return visit(root)
836
+ }
837
+
838
+ function addJsxAttribute(root, attribute, factory) {
839
+ if (ts.isJsxSelfClosingElement(root)) {
840
+ return factory.updateJsxSelfClosingElement(root, root.tagName, root.typeArguments, factory.updateJsxAttributes(root.attributes, [attribute, ...root.attributes.properties]))
841
+ }
842
+ const opening = factory.updateJsxOpeningElement(root.openingElement, root.openingElement.tagName, root.openingElement.typeArguments, factory.updateJsxAttributes(root.openingElement.attributes, [attribute, ...root.openingElement.attributes.properties]))
843
+ return factory.updateJsxElement(root, opening, root.children, root.closingElement)
844
+ }
845
+
846
+ function jsxTagName(node) {
847
+ return ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
848
+ }
849
+
850
+ function isJsxSyntaxIdentifier(node) {
851
+ const parent = node.parent
852
+ return (ts.isJsxOpeningElement(parent) || ts.isJsxClosingElement(parent) || ts.isJsxSelfClosingElement(parent)) && parent.tagName === node || ts.isJsxAttribute(parent) && parent.name === node
853
+ }
854
+
855
+ function isFunctionLike(node) {
856
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
857
+ }
858
+
859
+ function isExportedDeclaration(node) {
860
+ const statement = ts.isVariableDeclaration(node) ? node.parent?.parent : node
861
+ return statement?.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
862
+ }
863
+
864
+ function jsxTagUses(root, name) {
865
+ const uses = []
866
+ const visit = node => {
867
+ const tag = ts.isJsxElement(node) ? node.openingElement.tagName : ts.isJsxSelfClosingElement(node) ? node.tagName : undefined
868
+ if (tag && ts.isIdentifier(tag) && tag.text === name) uses.push(node)
869
+ ts.forEachChild(node, visit)
870
+ }
871
+ visit(root)
872
+ return uses
683
873
  }
684
874
 
685
875
  const pureListMethods = new Set(["at", "charAt", "charCodeAt", "concat", "endsWith", "includes", "indexOf", "join", "lastIndexOf", "padEnd", "padStart", "repeat", "replace", "replaceAll", "slice", "startsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimEnd", "trimStart"])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",