@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 +14 -1
- package/framework/README.md +1 -1
- package/framework/build.mjs +92 -33
- package/framework/core.d.ts +3 -2
- package/framework/core.mjs +3 -2
- package/package.json +1 -1
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
|
-
|
|
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.
|
package/framework/README.md
CHANGED
|
@@ -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.
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 ((
|
|
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
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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 (
|
|
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))
|
|
1786
|
+
if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
|
|
1787
|
+
cleanup = true
|
|
1788
|
+
cleanups.push(expression)
|
|
1789
|
+
}
|
|
1790
|
+
else invalid = node
|
|
1747
1791
|
}
|
|
1748
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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,
|
package/framework/core.d.ts
CHANGED
|
@@ -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
|
package/framework/core.mjs
CHANGED
|
@@ -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) {
|