@kudzujs/core 0.7.2 → 0.7.4
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 +2 -2
- package/RELEASES.md +48 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +156 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**0.7.
|
|
13
|
+
**0.7.4:** Memoized collection pipelines. React `useMemo` may retain analyzable `filter`, `map`, `flatMap`, and `Array.from` pipelines while compiling to existing keyed-list selectors without React, a VDOM, or hydration. See [release notes](./RELEASES.md#074---memoized-collection-pipelines).
|
|
14
14
|
|
|
15
15
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
16
16
|
|
|
@@ -74,7 +74,7 @@ export default function Header() {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. Inline `useCallback(function, literalDependencies)` is erased to its function
|
|
77
|
+
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks may return one expression over primitive literals/direct local state or an analyzable `filter`, `map`, `flatMap`, and `Array.from` collection pipeline. Scalar expressions inline into existing bindings; collection pipelines lower to existing keyed-list selectors and preserve row identity. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
|
|
78
78
|
|
|
79
79
|
Create `src/pages/index.tsx`:
|
|
80
80
|
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.4 - Memoized collection pipelines
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.4 lets ordinary React/Vite source retain analyzable collection work inside `useMemo` while reusing the existing keyed-list selector and DOM identity model.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.4
|
|
8
|
+
|
|
9
|
+
- Inline `useMemo` callbacks accept collection pipelines rooted in direct local array state.
|
|
10
|
+
- Existing `filter`, direct-property `flatMap`, and `Array.from` selector analysis is reused without a browser memo cache.
|
|
11
|
+
- Intermediate `.map()` calls lower to the existing `Array.from(source, mapper)` selector operation.
|
|
12
|
+
- Memo locals are removed from emitted server modules after their uses are inlined, so state signals never execute array methods during rendering.
|
|
13
|
+
- State updates preserve keyed row DOM identity while adding, removing, filtering, mapping, and reordering selected values.
|
|
14
|
+
- Collection callbacks must be synchronous arrows with plain `(item)` or `(item, index)` identifier parameters; async, rest, default, and optional parameters fail with source locations.
|
|
15
|
+
- The React/Vite fixture verifies `filter + map`, selector generation, browser updates, and retained identity for an existing row.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
Memoized collections use the existing statically analyzable collection subset. Arbitrary callbacks, external captures, asynchronous transforms, getters, and general-purpose memo caching remain unsupported. React, a VDOM, hydration, and a retained browser component tree are not emitted.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.4
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 0.7.3 - React memo normalization
|
|
28
|
+
|
|
29
|
+
Kudzu 0.7.3 accepts common React memo authoring forms while preserving build-time components, direct state bindings, static HTML, and capability-only JavaScript.
|
|
30
|
+
|
|
31
|
+
### New in 0.7.3
|
|
32
|
+
|
|
33
|
+
- `memo(Component)` and aliased or direct-member equivalents lower to same-file build-time function components.
|
|
34
|
+
- Inline `useCallback` wrappers continue to lower directly to analyzable handler functions without a browser memo cache.
|
|
35
|
+
- Inline synchronous `useMemo` callbacks may return one expression over primitive literals and direct local state.
|
|
36
|
+
- Memoized state expressions are inlined at same-component JSX uses so existing bindings update them without component rerenders.
|
|
37
|
+
- Static pages wrapped in `memo` remain JavaScript-free.
|
|
38
|
+
- Component shadowing, impure expressions, incomplete state dependencies, duplicate memo locals, and nested memo-local captures fail with source locations.
|
|
39
|
+
- The React/Vite app fixture verifies repeated counter updates, derived `Double 2`/`Double 4` output, CSS and SVG assets, mount effects, and a zero-JavaScript static route.
|
|
40
|
+
|
|
41
|
+
### Boundary
|
|
42
|
+
|
|
43
|
+
`memo` identifiers must name unshadowed same-file top-level function components. `useMemo` locals must use unique `const` declarations, may reference only direct local state and primitive literals, and cannot cross nested function boundaries. No browser component cache, React runtime, VDOM, hydration, or retained component tree is emitted.
|
|
44
|
+
|
|
45
|
+
### Upgrade
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @kudzujs/core@^0.7.3
|
|
49
|
+
```
|
|
50
|
+
|
|
3
51
|
## 0.7.2 - React hook normalization
|
|
4
52
|
|
|
5
53
|
Kudzu 0.7.2 accepts more ordinary React/Vite hook syntax while compiling through the existing static HTML and direct DOM capability paths.
|
package/framework/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
|
|
4
4
|
|
|
5
|
-
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, inline `useCallback`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation.
|
|
5
|
+
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
7
|
- `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
|
|
8
8
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
package/framework/build.mjs
CHANGED
|
@@ -1746,6 +1746,7 @@ function hasReactModuleReference(source, file) {
|
|
|
1746
1746
|
|
|
1747
1747
|
function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
1748
1748
|
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1749
|
+
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
1749
1750
|
const aliases = new Map()
|
|
1750
1751
|
const reactObjects = new Set()
|
|
1751
1752
|
for (const statement of sourceFile.statements) {
|
|
@@ -1755,8 +1756,8 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1755
1756
|
if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
|
|
1756
1757
|
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
|
|
1757
1758
|
const imported = (entry.propertyName ?? entry.name).text
|
|
1758
|
-
if (!entry.isTypeOnly && (supported.has(imported) || imported
|
|
1759
|
-
else if (!entry.isTypeOnly &&
|
|
1759
|
+
if (!entry.isTypeOnly && (supported.has(imported) || erased.has(imported))) aliases.set(entry.name.text, imported)
|
|
1760
|
+
else if (!entry.isTypeOnly && /^use[A-Z]/.test(imported)) throw sourceNodeError(entry, sourceFile, `React ${imported} is not supported by Kudzu migration input`)
|
|
1760
1761
|
}
|
|
1761
1762
|
}
|
|
1762
1763
|
if (!aliases.size && !reactObjects.size) return sourceFile
|
|
@@ -1786,17 +1787,77 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1786
1787
|
if (ts.isIdentifier(node) && reactObjects.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "React default or namespace imports may only be used for direct supported members or React.Fragment")
|
|
1787
1788
|
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
1788
1789
|
const name = node.name.text
|
|
1789
|
-
if (name !== "Fragment" && !(ts.isCallExpression(node.parent) && node.parent.expression === node && (supported.has(name) || name
|
|
1790
|
+
if (name !== "Fragment" && !(ts.isCallExpression(node.parent) && node.parent.expression === node && (supported.has(name) || erased.has(name)))) throw sourceNodeError(node, sourceFile, `React.${name} is not supported; use a directly supported hook call or React.Fragment`)
|
|
1790
1791
|
}
|
|
1791
1792
|
ts.forEachChild(node, validate)
|
|
1792
1793
|
}
|
|
1793
1794
|
validate(sourceFile)
|
|
1794
1795
|
|
|
1796
|
+
const memoLocals = new Map()
|
|
1797
|
+
const collectMemoLocals = node => {
|
|
1798
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && migrationCallName(node.initializer) === "useMemo") {
|
|
1799
|
+
if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "React useMemo() local values must use const declarations")
|
|
1800
|
+
const callback = node.initializer.arguments[0]
|
|
1801
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
1802
|
+
const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
|
|
1803
|
+
const owner = nearestFunction(node)
|
|
1804
|
+
if (owner && expression) {
|
|
1805
|
+
const entries = memoLocals.get(owner) ?? new Map()
|
|
1806
|
+
if (entries.has(node.name.text)) throw sourceNodeError(node.name, sourceFile, `React useMemo() local ${JSON.stringify(node.name.text)} must be unique within its component`)
|
|
1807
|
+
entries.set(node.name.text, { declaration: node, expression })
|
|
1808
|
+
memoLocals.set(owner, entries)
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
ts.forEachChild(node, collectMemoLocals)
|
|
1813
|
+
}
|
|
1814
|
+
collectMemoLocals(sourceFile)
|
|
1815
|
+
const memoLocalIsShadowed = (node, owner, entry) => {
|
|
1816
|
+
if (isShadowedByParameter(node, owner)) return true
|
|
1817
|
+
const declarationStatement = entry.declaration.parent?.parent
|
|
1818
|
+
for (let current = node.parent; current && current !== owner; current = current.parent) {
|
|
1819
|
+
if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
|
|
1820
|
+
if (ts.isBlock(current) && current.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text))) return true
|
|
1821
|
+
if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statement !== declarationStatement && statementDeclaresName(statement, node.text)))) return true
|
|
1822
|
+
if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
|
|
1823
|
+
if ((ts.isForStatement(current) || ts.isForInStatement(current) || ts.isForOfStatement(current)) && loopDeclaresName(current, node.text)) return true
|
|
1824
|
+
}
|
|
1825
|
+
return false
|
|
1826
|
+
}
|
|
1827
|
+
for (const [owner, entries] of memoLocals) for (const [name, entry] of entries) {
|
|
1828
|
+
const visit = node => {
|
|
1829
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && nearestFunctionLike(node) !== owner && !memoLocalIsShadowed(node, owner, entry)) throw sourceNodeError(node, sourceFile, `React useMemo() local ${JSON.stringify(name)} cannot be captured by a nested function`)
|
|
1830
|
+
ts.forEachChild(node, visit)
|
|
1831
|
+
}
|
|
1832
|
+
visit(owner.body)
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1795
1835
|
const required = new Set()
|
|
1796
1836
|
const imported = new Set()
|
|
1797
1837
|
const visitor = node => {
|
|
1838
|
+
if (ts.isVariableStatement(node)) {
|
|
1839
|
+
const entries = memoLocals.get(nearestFunction(node))
|
|
1840
|
+
if (entries) {
|
|
1841
|
+
for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && entries.has(declaration.name.text) && declaration.initializer) ts.visitNode(declaration.initializer, visitor)
|
|
1842
|
+
const declarations = node.declarationList.declarations.filter(declaration => !ts.isIdentifier(declaration.name) || !entries.has(declaration.name.text))
|
|
1843
|
+
if (!declarations.length) return undefined
|
|
1844
|
+
if (declarations.length !== node.declarationList.declarations.length) return factory.updateVariableStatement(node, node.modifiers, factory.updateVariableDeclarationList(node.declarationList, declarations.map(declaration => ts.visitEachChild(declaration, visitor, context))))
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
|
|
1848
|
+
const owner = nearestFunctionLike(node)
|
|
1849
|
+
const entry = memoLocals.get(owner)?.get(node.text)
|
|
1850
|
+
if (entry && !memoLocalIsShadowed(node, owner, entry)) return ts.visitNode(cloneAst(entry.expression, factory, context), visitor)
|
|
1851
|
+
}
|
|
1798
1852
|
if (ts.isCallExpression(node)) {
|
|
1799
1853
|
const name = migrationCallName(node)
|
|
1854
|
+
if (name === "memo") {
|
|
1855
|
+
if (node.arguments.length !== 1 || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]) || ts.isIdentifier(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React memo() requires exactly one function component or component identifier")
|
|
1856
|
+
if (ts.isIdentifier(node.arguments[0]) && isShadowedIdentifier(node.arguments[0], sourceFile)) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() component identifiers must resolve to an unshadowed same-file top-level function")
|
|
1857
|
+
const component = ts.isIdentifier(node.arguments[0]) ? reactMemoComponentExpression(node.arguments[0], sourceFile, factory, context) : node.arguments[0]
|
|
1858
|
+
if (!component) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() identifiers must name a same-file top-level function component")
|
|
1859
|
+
return ts.visitNode(component, visitor)
|
|
1860
|
+
}
|
|
1800
1861
|
if (name === "useCallback") {
|
|
1801
1862
|
if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useCallback() requires an inline function and a literal dependency array")
|
|
1802
1863
|
const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
|
|
@@ -1807,6 +1868,28 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1807
1868
|
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1808
1869
|
return ts.visitNode(node.arguments[0], visitor)
|
|
1809
1870
|
}
|
|
1871
|
+
if (name === "useMemo") {
|
|
1872
|
+
if (node.arguments.length !== 2 || !ts.isArrayLiteralExpression(node.arguments[1]) || !(ts.isArrowFunction(node.arguments[0]) || ts.isFunctionExpression(node.arguments[0]))) throw sourceNodeError(node, sourceFile, "React useMemo() requires an inline function and a literal dependency array")
|
|
1873
|
+
const callback = node.arguments[0]
|
|
1874
|
+
if (callback.parameters.length || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, sourceFile, "React useMemo() callback must be synchronous and parameterless")
|
|
1875
|
+
const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
|
|
1876
|
+
if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
|
|
1877
|
+
const expression = lowerReactMemoCollectionExpression(reactMemoExpression(callback), factory)
|
|
1878
|
+
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
1879
|
+
const owner = nearestFunction(node)
|
|
1880
|
+
const states = owner ? ownerStateNames(owner) : new Set()
|
|
1881
|
+
const collectionState = expression && reactMemoCollectionState(expression, states, sourceFile)
|
|
1882
|
+
if (!expression || !collectionState && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
|
|
1883
|
+
if (!collectionState) {
|
|
1884
|
+
const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
|
|
1885
|
+
if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
|
|
1886
|
+
}
|
|
1887
|
+
const stale = collectionState
|
|
1888
|
+
? !dependencies.has(collectionState) ? collectionState : undefined
|
|
1889
|
+
: [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
|
|
1890
|
+
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1891
|
+
return ts.visitNode(expression, visitor)
|
|
1892
|
+
}
|
|
1810
1893
|
if (name && supported.has(name)) {
|
|
1811
1894
|
required.add(name)
|
|
1812
1895
|
return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
|
|
@@ -1820,7 +1903,7 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1820
1903
|
const entries = []
|
|
1821
1904
|
for (const entry of bindings.elements) {
|
|
1822
1905
|
const name = (entry.propertyName ?? entry.name).text
|
|
1823
|
-
if (!entry.isTypeOnly && name
|
|
1906
|
+
if (!entry.isTypeOnly && erased.has(name)) continue
|
|
1824
1907
|
if (!entry.isTypeOnly && supported.has(name)) {
|
|
1825
1908
|
if (imported.has(name)) continue
|
|
1826
1909
|
imported.add(name)
|
|
@@ -1866,11 +1949,73 @@ function isReactCallbackDependency(node) {
|
|
|
1866
1949
|
return ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
|
|
1867
1950
|
}
|
|
1868
1951
|
|
|
1952
|
+
function reactMemoExpression(callback) {
|
|
1953
|
+
if (!ts.isBlock(callback.body)) return callback.body
|
|
1954
|
+
if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0])) return undefined
|
|
1955
|
+
return callback.body.statements[0].expression
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function lowerReactMemoCollectionExpression(expression, factory) {
|
|
1959
|
+
if (!expression) return undefined
|
|
1960
|
+
const visit = node => {
|
|
1961
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && node.arguments.length === 1) {
|
|
1962
|
+
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Array"), "from"), undefined, [visit(node.expression.expression), node.arguments[0]])
|
|
1963
|
+
}
|
|
1964
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["filter", "flatMap"].includes(node.expression.name.text)) {
|
|
1965
|
+
return factory.updateCallExpression(node, factory.updatePropertyAccessExpression(node.expression, visit(node.expression.expression), node.expression.name), node.typeArguments, node.arguments)
|
|
1966
|
+
}
|
|
1967
|
+
if (isArrayFromCall(node)) return factory.updateCallExpression(node, node.expression, node.typeArguments, [visit(node.arguments[0]), ...node.arguments.slice(1)])
|
|
1968
|
+
return node
|
|
1969
|
+
}
|
|
1970
|
+
return visit(expression)
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
function reactMemoCollectionState(expression, states, sourceFile) {
|
|
1974
|
+
const setters = new Map([...states].map(state => [state, state]))
|
|
1975
|
+
const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
|
|
1976
|
+
return renderedCollectionSource(expression, setters, undefined, fail, new Set())?.state?.text
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
|
|
1980
|
+
for (const statement of sourceFile.statements) {
|
|
1981
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
|
|
1982
|
+
const clone = cloneAst(statement, factory, context)
|
|
1983
|
+
return factory.createFunctionExpression(clone.modifiers?.filter(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword), clone.asteriskToken, clone.name, clone.typeParameters, clone.parameters, clone.type, clone.body)
|
|
1984
|
+
}
|
|
1985
|
+
if (!ts.isVariableStatement(statement)) continue
|
|
1986
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === identifier.text)
|
|
1987
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return cloneAst(declaration.initializer, factory, context)
|
|
1988
|
+
}
|
|
1989
|
+
return undefined
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function isPureReactMemoExpression(node) {
|
|
1993
|
+
node = unwrapExpression(node)
|
|
1994
|
+
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return true
|
|
1995
|
+
if (ts.isParenthesizedExpression(node)) return isPureReactMemoExpression(node.expression)
|
|
1996
|
+
if (ts.isPrefixUnaryExpression(node)) return ![ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) && isPureReactMemoExpression(node.operand)
|
|
1997
|
+
if (ts.isBinaryExpression(node)) return node.operatorToken.kind < ts.SyntaxKind.FirstAssignment && isPureReactMemoExpression(node.left) && isPureReactMemoExpression(node.right)
|
|
1998
|
+
if (ts.isConditionalExpression(node)) return isPureReactMemoExpression(node.condition) && isPureReactMemoExpression(node.whenTrue) && isPureReactMemoExpression(node.whenFalse)
|
|
1999
|
+
if (ts.isTemplateExpression(node)) return node.templateSpans.every(span => isPureReactMemoExpression(span.expression))
|
|
2000
|
+
return false
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
function reactMemoReferenceNames(root) {
|
|
2004
|
+
const names = new Set()
|
|
2005
|
+
const visit = node => {
|
|
2006
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) names.add(node.text)
|
|
2007
|
+
ts.forEachChild(node, visit)
|
|
2008
|
+
}
|
|
2009
|
+
visit(root)
|
|
2010
|
+
return names
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1869
2013
|
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
|
|
1870
2014
|
return context => sourceFile => {
|
|
1871
2015
|
const factory = context.factory
|
|
1872
2016
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
1873
2017
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
2018
|
+
ts.setParentRecursive(sourceFile, false)
|
|
1874
2019
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
1875
2020
|
ts.setParentRecursive(sourceFile, false)
|
|
1876
2021
|
rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
|
|
@@ -1881,6 +2026,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1881
2026
|
let imported = importedSources.get(target)
|
|
1882
2027
|
if (!imported) {
|
|
1883
2028
|
imported = normalizeReactMigrationSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
2029
|
+
ts.setParentRecursive(imported, false)
|
|
1884
2030
|
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
1885
2031
|
ts.setParentRecursive(imported, false)
|
|
1886
2032
|
importedSources.set(target, imported)
|
|
@@ -2787,7 +2933,7 @@ function isArrayFromCall(value) {
|
|
|
2787
2933
|
}
|
|
2788
2934
|
|
|
2789
2935
|
function collectionParameters(callback, label, fail) {
|
|
2790
|
-
if (!ts.isArrowFunction(callback) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name))) fail(callback, `${label} callback must be
|
|
2936
|
+
if (!ts.isArrowFunction(callback) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || callback.parameters.length < 1 || callback.parameters.length > 2 || callback.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) fail(callback, `${label} callback must be a synchronous arrow function with (item) or (item, index) identifier parameters`)
|
|
2791
2937
|
return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
|
|
2792
2938
|
}
|
|
2793
2939
|
|
|
@@ -3725,6 +3871,11 @@ function nearestFunction(node) {
|
|
|
3725
3871
|
return undefined
|
|
3726
3872
|
}
|
|
3727
3873
|
|
|
3874
|
+
function nearestFunctionLike(node) {
|
|
3875
|
+
for (let current = node.parent; current; current = current.parent) if (isFunctionLike(current)) return current
|
|
3876
|
+
return undefined
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3728
3879
|
function isShadowedByParameter(node, scopeRoot) {
|
|
3729
3880
|
for (let current = node.parent; current; current = current.parent) {
|
|
3730
3881
|
if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
|