@kudzujs/core 0.5.1 → 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,19 +264,32 @@ const rows = items.map(item =>
264
264
  return <ul>{rows}</ul>
265
265
  ```
266
266
 
267
- The root may also be a same-file row component that directly returns one intrinsic element and receives the whole item through one prop:
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
269
  ```tsx
270
- function ItemRow({ item }: { item: Item }) {
271
- return <li>{item.name}</li>
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>
272
280
  }
273
281
 
274
- const rows = items.map(item => <ItemRow key={item.id} item={item} />)
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
+ />)
275
288
  ```
276
289
 
277
- 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.
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.
278
291
 
279
- 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 same-file row component, and `key={item.<field>}`. A row component must directly destructure the whole item prop, directly return one intrinsic element, and be used only by that list. 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, lists, or component tags, item spreads, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
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>`.
280
293
 
281
294
  ## Normal JavaScript
282
295
 
@@ -448,7 +461,7 @@ The list starts with 1,000 keyed items, then updates every label, reverses the o
448
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 |
449
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 |
450
463
 
451
- An intrinsic-root versus 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 444 ms and 441 ms. Browser operation medians totaled 23.8 ms and 25.4 ms respectively; because the deployed HTML and JavaScript are identical, that 1.6 ms difference is measurement variance rather than component runtime overhead.
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.
452
465
 
453
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.
454
467
 
@@ -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,21 +471,66 @@ function createKudzuTransformer(nativeHandlers, reactiveBindings, listExpression
466
471
  if (uses.length) listLocalUses.set(uses[0], parts)
467
472
  }
468
473
  }
469
- const renderedLists = new WeakMap()
474
+ const rawRenderedLists = []
470
475
  const collectRenderedLists = node => {
471
476
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
472
477
  const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction))
473
- if (parts) {
474
- if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
475
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, functions)
476
- renderedLists.set(node, parts)
477
- }
478
+ if (parts) rawRenderedLists.push({ node, parts })
478
479
  }
479
480
  ts.forEachChild(node, collectRenderedLists)
480
481
  }
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
+ }
482
529
 
483
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
+
484
534
  if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text.startsWith(".")) {
485
535
  return factory.updateImportDeclaration(node, node.modifiers, node.importClause, factory.createStringLiteral(modulePath(node.moduleSpecifier.text)), node.attributes)
486
536
  }
@@ -639,35 +689,13 @@ function keyedListParts(expression, setters) {
639
689
  return { state, callback, root, item: callback.parameters[0].name.text, keyField: field }
640
690
  }
641
691
 
642
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, functions) {
692
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
643
693
  const fail = (node, message) => {
644
694
  const position = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
645
695
  throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} ${message}`)
646
696
  }
647
- let root = parts.root
648
- let item = parts.item
649
- const rootTag = ts.isJsxElement(root) ? root.openingElement.tagName : root.tagName
650
- if (ts.isIdentifier(rootTag) && rootTag.text[0] === rootTag.text[0].toUpperCase()) {
651
- const component = functions.get(rootTag.text)
652
- if (!component) fail(root, `Keyed list component ${rootTag.text} must be declared in the same file`)
653
- const uses = jsxTagUses(sourceFile, rootTag.text)
654
- if (uses.length !== 1 || uses[0] !== root) fail(root, `Keyed list component ${rootTag.text} may only be used as this list root`)
655
- const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
656
- let itemProp
657
- for (const attribute of attributes.properties) {
658
- if (ts.isJsxSpreadAttribute(attribute)) {
659
- if (referencesIdentifier(attribute.expression, item)) fail(attribute, "Keyed list item spreads are not supported")
660
- continue
661
- }
662
- if (attribute.name.getText() === "key" || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression || !referencesIdentifier(attribute.initializer.expression, item)) continue
663
- if (!ts.isIdentifier(attribute.initializer.expression) || attribute.initializer.expression.text !== item || itemProp) fail(attribute, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
664
- itemProp = attribute.name.getText()
665
- }
666
- if (ts.isJsxElement(root) && root.children.some(child => referencesIdentifier(child, item))) fail(root, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
667
- if (!itemProp) fail(root, `Keyed list component ${rootTag.text} must receive the whole item through one direct prop`)
668
- item = componentItemParameter(component, itemProp, node => fail(node, `Keyed list component ${rootTag.text} must destructure its item prop`))
669
- root = componentJsxRoot(component, node => fail(node, `Keyed list component ${rootTag.text} must return one JSX element`))
670
- }
697
+ const root = parts.root
698
+ const item = parts.item
671
699
  const validateElement = node => {
672
700
  const tag = ts.isJsxElement(node) ? node.openingElement.tagName : node.tagName
673
701
  if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
@@ -717,24 +745,120 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
717
745
  visit(root)
718
746
  }
719
747
 
720
- function componentItemParameter(component, prop, fail) {
721
- if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component)
722
- const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && !entry.initializer && (entry.propertyName ?? entry.name).getText() === prop)
723
- if (!element || !ts.isIdentifier(element.name)) fail(component.parameters[0])
724
- return element.name.text
725
- }
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}"`)
726
778
 
727
- function componentJsxRoot(component, fail) {
779
+ let returned
780
+ const calculations = []
728
781
  if (!ts.isBlock(component.body)) {
729
- const root = unwrapExpression(component.body)
730
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(component.body)
731
- return root
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
732
796
  }
733
- if (component.body.statements.length !== 1 || !ts.isReturnStatement(component.body.statements[0]) || !component.body.statements[0].expression) fail(component.body)
734
- const statement = component.body.statements[0]
735
- const root = unwrapExpression(statement.expression)
736
- if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(statement)
737
- return root
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
738
862
  }
739
863
 
740
864
  function jsxTagUses(root, name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.5.1",
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",