@kudzujs/core 0.5.7 → 0.5.8

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
@@ -346,10 +346,23 @@ useEffect(async () => {
346
346
 
347
347
  Kudzu does not execute the effect during static rendering and does not ship the component. It emits one route-specific effect entry that invokes the compiled callback against existing logical state and direct DOM commit capabilities. Effects may update reactive text, attributes, conditions, and keyed lists. Multiple effects start independently in source order, and one synchronous or asynchronous failure is reported without suppressing later effects.
348
348
 
349
- Only inline block-bodied callbacks with a literal empty dependency array are supported. Dependencies, cleanup or other return values, callback parameters, and non-serializable captures are rejected at build time. Pages without effects receive no effect entry and retain their existing output.
349
+ An effect may directly return an inline cleanup function:
350
+
351
+ ```tsx
352
+ useEffect(() => {
353
+ const onResize = () => console.log(window.innerWidth)
354
+ window.addEventListener("resize", onResize)
355
+
356
+ return () => window.removeEventListener("resize", onResize)
357
+ }, [])
358
+ ```
359
+
360
+ Cleanup runs once when the document leaves outside the browser back-forward cache. Effect-local resources and component state read by nested cleanup closures retain their mount-time values. Cleanup failures are isolated so later cleanups still run. Only inline block-bodied callbacks with a literal empty dependency array are supported. Dependencies, named or dynamically obtained cleanup functions, cleanup parameters or generators, other return values, callback parameters, and non-serializable captures are rejected at build time. Async effects cannot return cleanup functions; the cleanup itself may be async. Pages without effects receive no effect entry, and effects without cleanup retain their smaller runtime output.
350
361
 
351
362
  A matched mount-fetch benchmark renders a title and two keyed rows from local JSON. With one warm-up and seven rotating clean builds, Kudzu shipped initial HTML, 3.4 KB initial JS gzip, 8.1 KB total output, and built in 374 ms. React CSR shipped no initial content, 59.3 KB initial JS gzip, 189.2 KB total output, and built in 992 ms. Hand-written ESM shipped 534 B initial JS gzip, 1.2 KB total output, and built in 210 ms. Fresh-profile Chrome medians to loaded data were 157.9 ms, 166.5 ms, and 153.4 ms respectively.
352
363
 
364
+ A matched resize-listener cleanup fixture, measured with the same warm-up and seven rotating clean builds, shipped 1.2 KB JavaScript gzip and built in 402 ms with Kudzu. Svelte shipped 10.1 KB and built in 861 ms, Vue shipped 23.6 KB and built in 768 ms, and React shipped 59.1 KB and built in 1,058 ms. Kudzu and the 127 B hand-written Astro baseline emitted initial HTML; the CSR fixtures did not.
365
+
353
366
  ## Normal JavaScript
354
367
 
355
368
  Command-only setters use the smallest optimized path. Conditions, local variables, browser globals, events, and `async`/`await` compile to external ESM without `eval`, `new Function`, or inline executable code.
@@ -13,6 +13,6 @@
13
13
  - `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
14
14
  - `*.d.ts`: public TypeScript and JSX declarations.
15
15
 
16
- Static routes receive no browser runtime. Command routes receive `runtime.js`; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`; mount effects add `effect-runtime.js` and one route-specific entry. List builds remove unused text-range, attribute, event, expression, condition, seed, and mount branches. Effect builds omit capture deserialization entirely when every effect scope is empty. Capability runtimes share state and lifecycle hooks through `shared-runtime.js`. Generated evaluators and their bundled relative TypeScript helpers live under `dist/assets/handlers/`; shared helper chunks are emitted only when multiple handler entries need them. Runtime fallback rewrites are ordered by specificity in `.kudzu/kudzu-plan.json` and passed to `afterBuild()`; exact static files take precedence in development. The dev server derives stable state identities from route-unique state variable names in each route plan; every state sharing a duplicate name is omitted. It then injects its SSE reload, short-lived full-URL-scoped logical-state snapshot, and build-error client into responses only, never into `dist/`. Snapshots are consumed even when the next page is static or broken. Reload restoration covers compatible framework state, not uncontrolled DOM state, focus, selection, or imperative mutations.
16
+ Static routes receive no browser runtime. Command routes receive `runtime.js`; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`; mount effects add `effect-runtime.js` and one route-specific entry. Effect cleanup integrates with shared unmount hooks when present and otherwise disposes directly on non-persisted `pagehide`; unrelated routes and effects retain their existing runtime. List builds remove unused text-range, attribute, event, expression, condition, seed, and mount branches. Effect builds omit capture deserialization entirely when every effect scope is empty. Capability runtimes share state and lifecycle hooks through `shared-runtime.js`. Generated evaluators and their bundled relative TypeScript helpers live under `dist/assets/handlers/`; shared helper chunks are emitted only when multiple handler entries need them. Runtime fallback rewrites are ordered by specificity in `.kudzu/kudzu-plan.json` and passed to `afterBuild()`; exact static files take precedence in development. The dev server derives stable state identities from route-unique state variable names in each route plan; every state sharing a duplicate name is omitted. It then injects its SSE reload, short-lived full-URL-scoped logical-state snapshot, and build-error client into responses only, never into `dist/`. Snapshots are consumed even when the next page is static or broken. Reload restoration covers compatible framework state, not uncontrolled DOM state, focus, selection, or imperative mutations.
17
17
 
18
18
  Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime.
@@ -244,6 +244,7 @@ function specializeNativeRuntime(source, events, modules) {
244
244
  }
245
245
 
246
246
  function printEffectEntry(effects, output, handlerModules, assetsDirectory, base, paramPath) {
247
+ const hasCleanup = effects.some(effect => effect.cleanup)
247
248
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
248
249
  const modules = moduleUrls.map(url => {
249
250
  const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
@@ -251,13 +252,15 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, base
251
252
  return module
252
253
  })
253
254
  const imports = [
254
- `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
255
+ hasCleanup
256
+ ? `import * as __kRuntime from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}\nconst { browserState, commitDom } = __kRuntime`
257
+ : `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu.js")))}`,
255
258
  `import { createEffectContext } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, "kudzu-effect.js")))}`,
256
259
  ...(paramPath ? [`import ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, paramPath)))}`] : []),
257
260
  ...modules.map((module, index) => `import * as __kEffectModule${index} from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, module.path)))}`)
258
261
  ]
259
262
  const entries = moduleUrls.map((url, index) => `[${JSON.stringify(url)}, __kEffectModule${index}]`).join(",")
260
- return `${imports.join("\n")}
263
+ if (!hasCleanup) return `${imports.join("\n")}
261
264
  const effects = ${inlineJson(effects)}
262
265
  const modules = new Map([${entries}])
263
266
  for (const effect of effects) {
@@ -268,6 +271,39 @@ for (const effect of effects) {
268
271
  console.error(error)
269
272
  }
270
273
  }`
274
+ return `${imports.join("\n")}
275
+ const effects = ${inlineJson(effects)}
276
+ const modules = new Map([${entries}])
277
+ const cleanups = []
278
+ for (const effect of effects) {
279
+ try {
280
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, effect.scope))
281
+ if (effect.cleanup && typeof result === "function") cleanups.push(result)
282
+ else if (result && typeof result.then === "function") result.catch(error => console.error(error))
283
+ } catch (error) {
284
+ console.error(error)
285
+ }
286
+ }
287
+ let cleaned = false
288
+ const dispose = root => {
289
+ if (root !== document || cleaned) return
290
+ cleaned = true
291
+ for (const cleanup of cleanups) {
292
+ try {
293
+ const result = cleanup()
294
+ if (result && typeof result.then === "function") result.catch(error => console.error(error))
295
+ } catch (error) {
296
+ console.error(error)
297
+ }
298
+ }
299
+ cleanups.length = 0
300
+ }
301
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
302
+ addEventListener("pagehide", event => {
303
+ if (event.persisted) return
304
+ if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
305
+ else dispose(document)
306
+ })`
271
307
  }
272
308
 
273
309
  function printParamEntry(schema, params, output, assetsDirectory, base) {
@@ -806,14 +842,18 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
806
842
  const [callback, dependencies] = node.arguments
807
843
  if (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) fail(callback, "useEffect() callback must be an inline function")
808
844
  if (ts.isFunctionExpression(callback) && callback.name) fail(callback, "useEffect() callback function must be anonymous")
845
+ if (callback.asteriskToken) fail(callback, "useEffect() callback cannot be a generator")
809
846
  if (callback.parameters.length) fail(callback, "useEffect() callback cannot declare parameters")
810
847
  if (!ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length) fail(dependencies, "useEffect() dependencies must be a literal empty array")
811
848
  if (!nearestFunction(node)) fail(node, "useEffect() cannot be used outside a Kudzu component")
812
- if (returnsCleanup(callback)) fail(callback, "useEffect() cleanup functions are not supported")
813
849
  if (!ts.isBlock(callback.body)) fail(callback, "useEffect() callback must use a block body")
814
- if (returnsEffectValue(callback)) fail(callback, "useEffect() return values are not supported")
850
+ const returns = effectReturns(callback)
851
+ if (returns.invalid) fail(returns.invalid, "useEffect() return values must be inline cleanup functions")
852
+ const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
853
+ if (invalidCleanup) fail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
854
+ if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) fail(callback, "useEffect() async callbacks cannot return cleanup functions")
815
855
  const setters = settersForNode(node, settersByFunction)
816
- const descriptor = compileNativeCallback(callback, setters, factory, effectHandlers, importBindings, clientImports, "effect", undefined, true)
856
+ const descriptor = compileNativeCallback(callback, setters, factory, effectHandlers, importBindings, clientImports, "effect", undefined, true, returns.cleanup)
817
857
  usesBehavior = true
818
858
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
819
859
  callback,
@@ -822,7 +862,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
822
862
  factory.createStringLiteral(descriptor.exportName),
823
863
  descriptor.states,
824
864
  descriptor.scope,
825
- factory.createStringLiteral(sourceLocation(node, sourceFile))
865
+ factory.createStringLiteral(sourceLocation(node, sourceFile)),
866
+ returns.cleanup ? factory.createTrue() : factory.createFalse()
826
867
  ])
827
868
  }
828
869
 
@@ -1240,7 +1281,7 @@ function isJsxSyntaxIdentifier(node) {
1240
1281
  }
1241
1282
 
1242
1283
  function isFunctionLike(node) {
1243
- return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)
1284
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node) || ts.isConstructorDeclaration(node)
1244
1285
  }
1245
1286
 
1246
1287
  function isDestructuredParameter(identifier, fn) {
@@ -1480,14 +1521,14 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
1480
1521
  ])
1481
1522
  }
1482
1523
 
1483
- function compileNativeCallback(expression, setters, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false) {
1524
+ function compileNativeCallback(expression, setters, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false) {
1484
1525
  const allCaptures = nativeCaptureNames(expression, setters)
1485
1526
  const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
1486
1527
  const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
1487
1528
  for (const entry of imports) clientImports.add(entry.target)
1488
1529
  const usedStates = nativeStateNames(expression, setters)
1489
1530
  const exportName = `${prefix}${entries.length}`
1490
- entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))) })
1531
+ entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), snapshotNested })
1491
1532
  const value = name => deferValues
1492
1533
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
1493
1534
  : factory.createIdentifier(name)
@@ -1586,6 +1627,7 @@ function isReferenceIdentifier(node) {
1586
1627
  if ((ts.isPropertyAccessExpression(parent) && parent.name === node) ||
1587
1628
  (ts.isPropertyAssignment(parent) && parent.name === node) ||
1588
1629
  (ts.isMethodDeclaration(parent) && parent.name === node) ||
1630
+ ((ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent)) && parent.name === node) ||
1589
1631
  (ts.isVariableDeclaration(parent) && parent.name === node) ||
1590
1632
  (ts.isParameter(parent) && parent.name === node) ||
1591
1633
  (ts.isFunctionDeclaration(parent) && parent.name === node) ||
@@ -1603,7 +1645,7 @@ function nearestFunction(node) {
1603
1645
 
1604
1646
  function isShadowedByParameter(node, scopeRoot) {
1605
1647
  for (let current = node.parent; current; current = current.parent) {
1606
- if ((ts.isFunctionDeclaration(current) || ts.isFunctionExpression(current) || ts.isArrowFunction(current)) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
1648
+ if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
1607
1649
  if (current === scopeRoot) break
1608
1650
  }
1609
1651
  return false
@@ -1733,33 +1775,24 @@ function sourceLocation(node, fallbackSource) {
1733
1775
  return `${sourceFile.fileName}:${position.line + 1}:${position.character + 1}`
1734
1776
  }
1735
1777
 
1736
- function returnsCleanup(callback) {
1737
- if (ts.isArrowFunction(callback) && !ts.isBlock(callback.body)) {
1738
- const body = unwrapExpression(callback.body)
1739
- return ts.isArrowFunction(body) || ts.isFunctionExpression(body)
1740
- }
1741
- let found = false
1778
+ function effectReturns(callback) {
1779
+ let cleanup = false
1780
+ let invalid
1781
+ const cleanups = []
1742
1782
  const visit = node => {
1743
- if (found || node !== callback.body && isFunctionLike(node)) return
1783
+ if (invalid || node !== callback.body && isFunctionLike(node)) return
1744
1784
  if (ts.isReturnStatement(node) && node.expression) {
1745
1785
  const expression = unwrapExpression(node.expression)
1746
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) found = true
1786
+ if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
1787
+ cleanup = true
1788
+ cleanups.push(expression)
1789
+ }
1790
+ else invalid = node
1747
1791
  }
1748
- if (!found) ts.forEachChild(node, visit)
1749
- }
1750
- visit(callback.body)
1751
- return found
1752
- }
1753
-
1754
- function returnsEffectValue(callback) {
1755
- let found = false
1756
- const visit = node => {
1757
- if (found || node !== callback.body && isFunctionLike(node)) return
1758
- if (ts.isReturnStatement(node) && node.expression) found = true
1759
- if (!found) ts.forEachChild(node, visit)
1792
+ if (!invalid) ts.forEachChild(node, visit)
1760
1793
  }
1761
1794
  visit(callback.body)
1762
- return found
1795
+ return { cleanup, cleanups, invalid }
1763
1796
  }
1764
1797
 
1765
1798
  function printClientImports(entries, handlerPath) {
@@ -1874,9 +1907,11 @@ function relativeModulePath(from, to) {
1874
1907
  return path.startsWith(".") ? path : `./${path}`
1875
1908
  }
1876
1909
 
1877
- function printNativeHandler({ exportName, expression, captures, setters }) {
1910
+ function printNativeHandler({ exportName, expression, captures, setters, snapshotNested }) {
1878
1911
  const factory = ts.factory
1879
1912
  const stateNames = new Set(setters.values())
1913
+ const snapshotNames = snapshotNested ? nestedStateNames(expression, setters) : new Set()
1914
+ const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
1880
1915
  const transformer = context => root => {
1881
1916
  const visitor = node => {
1882
1917
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
@@ -1893,9 +1928,11 @@ function printNativeHandler({ exportName, expression, captures, setters }) {
1893
1928
  return setterReference(factory, setters.get(node.text))
1894
1929
  }
1895
1930
  if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
1931
+ if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
1896
1932
  return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
1897
1933
  }
1898
1934
  if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
1935
+ if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
1899
1936
  return factory.createCallExpression(
1900
1937
  factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
1901
1938
  undefined,
@@ -1914,9 +1951,13 @@ function printNativeHandler({ exportName, expression, captures, setters }) {
1914
1951
  }
1915
1952
  const transformed = ts.transform(expression.body, [transformer])
1916
1953
  try {
1917
- const body = ts.isBlock(expression.body)
1954
+ let body = ts.isBlock(expression.body)
1918
1955
  ? transformed.transformed[0]
1919
1956
  : factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
1957
+ if (snapshots.size) body = factory.updateBlock(body, [
1958
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([...snapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(name)]))), ts.NodeFlags.Const)),
1959
+ ...body.statements
1960
+ ])
1920
1961
  const modifiers = [factory.createModifier(ts.SyntaxKind.ExportKeyword)]
1921
1962
  if (expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) modifiers.push(factory.createModifier(ts.SyntaxKind.AsyncKeyword))
1922
1963
  const declaration = factory.createFunctionDeclaration(
@@ -1934,6 +1975,24 @@ function printNativeHandler({ exportName, expression, captures, setters }) {
1934
1975
  }
1935
1976
  }
1936
1977
 
1978
+ function nestedStateNames(expression, setters) {
1979
+ const states = new Set(setters.values())
1980
+ const names = new Set()
1981
+ const visit = node => {
1982
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
1983
+ ts.forEachChild(node, visit)
1984
+ }
1985
+ visit(expression.body)
1986
+ return names
1987
+ }
1988
+
1989
+ function insideNestedFunction(node, root) {
1990
+ for (let current = node.parent; current && current !== root; current = current.parent) {
1991
+ if (isFunctionLike(current)) return true
1992
+ }
1993
+ return false
1994
+ }
1995
+
1937
1996
  function setterReference(factory, stateName) {
1938
1997
  return factory.createArrowFunction(
1939
1998
  undefined,
@@ -1,7 +1,8 @@
1
1
  export type StateSetter<T> = (value: T | ((previous: T) => T)) => void
2
+ export type EffectCleanup = () => void | Promise<void>
2
3
 
3
4
  export function useState<T>(initialValue: T): [T, StateSetter<T>]
4
- export function useEffect(effect: () => void | Promise<void>, dependencies: readonly []): void
5
+ export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly []): void
5
6
  export function useParams<Params extends Record<string, string> = Record<string, string>>(): Readonly<Params>
6
7
 
7
8
  export interface RefObject<T> {
@@ -70,7 +71,7 @@ export function renderPage<Props = Record<string, never>>(
70
71
  commands?: Array<[string, string, unknown]>
71
72
  native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
72
73
  }>
73
- effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }>
74
+ effects: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; cleanup?: true }>
74
75
  bindings: Array<{
75
76
  target: string
76
77
  state?: string
@@ -64,12 +64,12 @@ function createSignal(id, value) {
64
64
  }
65
65
  }
66
66
 
67
- export function useEffect(callback, dependencies, module, handler, states, scope, source) {
67
+ export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup) {
68
68
  if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
69
69
  if (typeof callback !== "function" || !Array.isArray(dependencies) || dependencies.length || !module || !handler) {
70
70
  throw new Error("useEffect() must be compiled with a literal empty dependency array")
71
71
  }
72
- renderContext.effects.push({ module, handler, states, scope, source })
72
+ renderContext.effects.push({ module, handler, states, scope, source, ...(cleanup ? { cleanup: true } : {}) })
73
73
  renderContext.hasBehaviors = true
74
74
  renderContext.hasEffects = true
75
75
  }
@@ -271,6 +271,7 @@ export async function renderPage(component, metadata = {}, props = {}) {
271
271
  return {
272
272
  module: effect.module,
273
273
  handler: effect.handler,
274
+ ...(effect.cleanup ? { cleanup: true } : {}),
274
275
  ...nativeDescriptor(effect.states.map(([name, read]) => [name, read()]), effect.scope.map(([name, read]) => [name, read()]))
275
276
  }
276
277
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.5.7",
3
+ "version": "0.5.8",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",