@kudzujs/core 0.7.1 → 0.7.3
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 +3 -3
- package/RELEASES.md +48 -0
- package/framework/README.md +1 -1
- package/framework/build.mjs +242 -1
- 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.3:** React memo normalization. Same-file `memo`, inline `useCallback`, and direct-state expression `useMemo` now compile to existing Kudzu capabilities without React, a VDOM, or hydration. See [release notes](./RELEASES.md#073---react-memo-normalization).
|
|
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 `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports
|
|
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 returning one expression over primitive literals and direct local state are inlined at same-component uses so existing bindings track that state. 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
|
|
|
@@ -225,7 +225,7 @@ const add = (title: string) => dispatch({ type: "add", title })
|
|
|
225
225
|
return <Input onSubmit={add} />
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
Kudzu substitutes the callback into the child's compiled event handler at build time. This is not general function-prop serialization: only one nested specialized callback boundary is supported, and
|
|
228
|
+
Kudzu substitutes the callback into the child's compiled event handler at build time. An inline React `useCallback` wrapper is erased before this analysis. This is not general function-prop serialization: only one nested specialized callback boundary is supported, and further forwarding, effects, component roots, and callback use outside event handlers are rejected.
|
|
229
229
|
|
|
230
230
|
Reducer dispatch and callback components may use destructured string, finite-number, boolean, or `null` defaults. A missing prop is replaced during specialization; object, array, computed, and function-call defaults remain unsupported:
|
|
231
231
|
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.3 - React memo normalization
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.3 accepts common React memo authoring forms while preserving build-time components, direct state bindings, static HTML, and capability-only JavaScript.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.3
|
|
8
|
+
|
|
9
|
+
- `memo(Component)` and aliased or direct-member equivalents lower to same-file build-time function components.
|
|
10
|
+
- Inline `useCallback` wrappers continue to lower directly to analyzable handler functions without a browser memo cache.
|
|
11
|
+
- Inline synchronous `useMemo` callbacks may return one expression over primitive literals and direct local state.
|
|
12
|
+
- Memoized state expressions are inlined at same-component JSX uses so existing bindings update them without component rerenders.
|
|
13
|
+
- Static pages wrapped in `memo` remain JavaScript-free.
|
|
14
|
+
- Component shadowing, impure expressions, incomplete state dependencies, duplicate memo locals, and nested memo-local captures fail with source locations.
|
|
15
|
+
- 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.
|
|
16
|
+
|
|
17
|
+
### Boundary
|
|
18
|
+
|
|
19
|
+
`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.
|
|
20
|
+
|
|
21
|
+
### Upgrade
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @kudzujs/core@^0.7.3
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 0.7.2 - React hook normalization
|
|
28
|
+
|
|
29
|
+
Kudzu 0.7.2 accepts more ordinary React/Vite hook syntax while compiling through the existing static HTML and direct DOM capability paths.
|
|
30
|
+
|
|
31
|
+
### New in 0.7.2
|
|
32
|
+
|
|
33
|
+
- Supported hooks imported from `react` may retain aliases such as `useState as useMenuState` and `useEffect as runEffect`.
|
|
34
|
+
- Default and namespace imports may use direct supported members such as `React.useState(...)`.
|
|
35
|
+
- Inline React `useCallback` wrappers with literal inert dependencies are erased to their callback before Kudzu specialization.
|
|
36
|
+
- Captured local state must appear in the callback dependency array, preserving an actionable boundary around stale React closures.
|
|
37
|
+
- Unsupported members, indirect hook references, computed React members, and effectful dependency expressions fail with source locations.
|
|
38
|
+
- Type-only React namespaces remain available to TypeScript and do not trigger runtime migration diagnostics.
|
|
39
|
+
- A React/Vite-shaped app fixture verifies CSS, SVG assets, a non-root base, aliased state/effects, member state, repeated callback updates, browser interaction, and a zero-JavaScript static route.
|
|
40
|
+
|
|
41
|
+
### Boundary
|
|
42
|
+
|
|
43
|
+
`useCallback` requires an inline function and a literal array containing only identifiers or primitive literals. `memo`, `useMemo`, React classes, indirect hook references, side-effect React imports, and dynamic React imports remain unsupported. React, a VDOM, hydration, and a retained browser component tree are never emitted.
|
|
44
|
+
|
|
45
|
+
### Upgrade
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm install @kudzujs/core@^0.7.2
|
|
49
|
+
```
|
|
50
|
+
|
|
3
51
|
## 0.7.1 - Vite-style landing assets
|
|
4
52
|
|
|
5
53
|
Kudzu 0.7.1 lets ordinary React/Vite landing-page source retain its common local stylesheet and static asset imports while preserving static HTML output and capability-only JavaScript.
|
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
|
|
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 `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 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
|
@@ -1744,10 +1744,243 @@ function hasReactModuleReference(source, file) {
|
|
|
1744
1744
|
return found
|
|
1745
1745
|
}
|
|
1746
1746
|
|
|
1747
|
+
function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
1748
|
+
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1749
|
+
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
1750
|
+
const aliases = new Map()
|
|
1751
|
+
const reactObjects = new Set()
|
|
1752
|
+
for (const statement of sourceFile.statements) {
|
|
1753
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "react") continue
|
|
1754
|
+
if (statement.importClause?.name) reactObjects.add(statement.importClause.name.text)
|
|
1755
|
+
const bindings = statement.importClause?.namedBindings
|
|
1756
|
+
if (bindings && ts.isNamespaceImport(bindings)) reactObjects.add(bindings.name.text)
|
|
1757
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) {
|
|
1758
|
+
const imported = (entry.propertyName ?? entry.name).text
|
|
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`)
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
if (!aliases.size && !reactObjects.size) return sourceFile
|
|
1764
|
+
|
|
1765
|
+
const migrationCallName = call => {
|
|
1766
|
+
if (ts.isIdentifier(call.expression) && aliases.has(call.expression.text) && !isShadowedIdentifier(call.expression, sourceFile)) return aliases.get(call.expression.text)
|
|
1767
|
+
if (ts.isPropertyAccessExpression(call.expression) && ts.isIdentifier(call.expression.expression) && reactObjects.has(call.expression.expression.text) && !isShadowedIdentifier(call.expression.expression, sourceFile)) return call.expression.name.text
|
|
1768
|
+
return undefined
|
|
1769
|
+
}
|
|
1770
|
+
const ownerStateNames = owner => {
|
|
1771
|
+
const names = new Set()
|
|
1772
|
+
const collect = node => {
|
|
1773
|
+
if (node !== owner && isFunctionLike(node)) return
|
|
1774
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ["useReducer", "useState"].includes(migrationCallName(node.initializer))) {
|
|
1775
|
+
const state = node.name.elements[0]
|
|
1776
|
+
if (state && ts.isBindingElement(state) && ts.isIdentifier(state.name)) names.add(state.name.text)
|
|
1777
|
+
}
|
|
1778
|
+
ts.forEachChild(node, collect)
|
|
1779
|
+
}
|
|
1780
|
+
collect(owner)
|
|
1781
|
+
return names
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
const validate = node => {
|
|
1785
|
+
if (ts.isTypeNode(node)) return
|
|
1786
|
+
if (ts.isIdentifier(node) && aliases.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, `Aliased React ${aliases.get(node.text)} must be called directly`)
|
|
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")
|
|
1788
|
+
if (ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && reactObjects.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) {
|
|
1789
|
+
const name = node.name.text
|
|
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`)
|
|
1791
|
+
}
|
|
1792
|
+
ts.forEachChild(node, validate)
|
|
1793
|
+
}
|
|
1794
|
+
validate(sourceFile)
|
|
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 = reactMemoExpression(callback)
|
|
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
|
+
|
|
1835
|
+
const required = new Set()
|
|
1836
|
+
const imported = new Set()
|
|
1837
|
+
const visitor = node => {
|
|
1838
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) {
|
|
1839
|
+
const owner = nearestFunctionLike(node)
|
|
1840
|
+
const entry = memoLocals.get(owner)?.get(node.text)
|
|
1841
|
+
if (entry && !memoLocalIsShadowed(node, owner, entry)) return ts.visitNode(cloneAst(entry.expression, factory, context), visitor)
|
|
1842
|
+
}
|
|
1843
|
+
if (ts.isCallExpression(node)) {
|
|
1844
|
+
const name = migrationCallName(node)
|
|
1845
|
+
if (name === "memo") {
|
|
1846
|
+
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")
|
|
1847
|
+
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")
|
|
1848
|
+
const component = ts.isIdentifier(node.arguments[0]) ? reactMemoComponentExpression(node.arguments[0], sourceFile, factory, context) : node.arguments[0]
|
|
1849
|
+
if (!component) throw sourceNodeError(node.arguments[0], sourceFile, "React memo() identifiers must name a same-file top-level function component")
|
|
1850
|
+
return ts.visitNode(component, visitor)
|
|
1851
|
+
}
|
|
1852
|
+
if (name === "useCallback") {
|
|
1853
|
+
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")
|
|
1854
|
+
const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
|
|
1855
|
+
if (dependency) throw sourceNodeError(dependency, sourceFile, "React useCallback() dependencies must be identifiers or primitive literals")
|
|
1856
|
+
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
1857
|
+
const owner = nearestFunction(node)
|
|
1858
|
+
const stale = owner && [...ownerStateNames(owner)].find(state => referenceIdentifiers(node.arguments[0], state).length && !dependencies.has(state))
|
|
1859
|
+
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useCallback() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1860
|
+
return ts.visitNode(node.arguments[0], visitor)
|
|
1861
|
+
}
|
|
1862
|
+
if (name === "useMemo") {
|
|
1863
|
+
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")
|
|
1864
|
+
const callback = node.arguments[0]
|
|
1865
|
+
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")
|
|
1866
|
+
const dependency = node.arguments[1].elements.find(entry => !isReactCallbackDependency(entry))
|
|
1867
|
+
if (dependency) throw sourceNodeError(dependency, sourceFile, "React useMemo() dependencies must be identifiers or primitive literals")
|
|
1868
|
+
const expression = reactMemoExpression(callback)
|
|
1869
|
+
if (!expression || !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression")
|
|
1870
|
+
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
1871
|
+
const owner = nearestFunction(node)
|
|
1872
|
+
const states = owner ? ownerStateNames(owner) : new Set()
|
|
1873
|
+
const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
|
|
1874
|
+
if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
|
|
1875
|
+
const stale = [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
|
|
1876
|
+
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1877
|
+
return ts.visitNode(expression, visitor)
|
|
1878
|
+
}
|
|
1879
|
+
if (name && supported.has(name)) {
|
|
1880
|
+
required.add(name)
|
|
1881
|
+
return factory.updateCallExpression(node, factory.createIdentifier(name), node.typeArguments, ts.visitNodes(node.arguments, visitor))
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
if (ts.isImportDeclaration(node) && !node.importClause?.isTypeOnly && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "react") {
|
|
1885
|
+
const clause = node.importClause
|
|
1886
|
+
if (!clause) return node
|
|
1887
|
+
let bindings = clause.namedBindings
|
|
1888
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
1889
|
+
const entries = []
|
|
1890
|
+
for (const entry of bindings.elements) {
|
|
1891
|
+
const name = (entry.propertyName ?? entry.name).text
|
|
1892
|
+
if (!entry.isTypeOnly && erased.has(name)) continue
|
|
1893
|
+
if (!entry.isTypeOnly && supported.has(name)) {
|
|
1894
|
+
if (imported.has(name)) continue
|
|
1895
|
+
imported.add(name)
|
|
1896
|
+
required.add(name)
|
|
1897
|
+
entries.push(factory.createImportSpecifier(false, undefined, factory.createIdentifier(name)))
|
|
1898
|
+
} else {
|
|
1899
|
+
entries.push(entry)
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
bindings = entries.length ? factory.updateNamedImports(bindings, entries) : undefined
|
|
1903
|
+
}
|
|
1904
|
+
if (!clause.name && !bindings) return undefined
|
|
1905
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, clause.name, bindings), node.moduleSpecifier, node.attributes)
|
|
1906
|
+
}
|
|
1907
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1908
|
+
}
|
|
1909
|
+
let normalized = ts.visitNode(sourceFile, visitor)
|
|
1910
|
+
const missing = [...required].filter(name => !imported.has(name)).sort()
|
|
1911
|
+
if (!missing.length) return normalized
|
|
1912
|
+
for (const name of missing) {
|
|
1913
|
+
const collision = sourceFile.statements.some(statement => statementDeclaresName(statement, name) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name) && statement.moduleSpecifier.text !== "react")
|
|
1914
|
+
if (collision) throw sourceNodeError(sourceFile, sourceFile, `React.${name} cannot be normalized because ${JSON.stringify(name)} is already declared`)
|
|
1915
|
+
}
|
|
1916
|
+
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports(missing.map(name => factory.createImportSpecifier(false, undefined, factory.createIdentifier(name))))), factory.createStringLiteral("react"))
|
|
1917
|
+
const statements = [...normalized.statements]
|
|
1918
|
+
const lastImport = statements.findLastIndex(statement => ts.isImportDeclaration(statement))
|
|
1919
|
+
statements.splice(lastImport + 1, 0, declaration)
|
|
1920
|
+
normalized = factory.updateSourceFile(normalized, statements)
|
|
1921
|
+
return normalized
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function importDeclarationNames(statement) {
|
|
1925
|
+
const names = []
|
|
1926
|
+
if (statement.importClause?.name) names.push(statement.importClause.name.text)
|
|
1927
|
+
const bindings = statement.importClause?.namedBindings
|
|
1928
|
+
if (bindings && ts.isNamespaceImport(bindings)) names.push(bindings.name.text)
|
|
1929
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) names.push(entry.name.text)
|
|
1930
|
+
return names
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
function isReactCallbackDependency(node) {
|
|
1934
|
+
node = unwrapExpression(node)
|
|
1935
|
+
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
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function reactMemoExpression(callback) {
|
|
1939
|
+
if (!ts.isBlock(callback.body)) return callback.body
|
|
1940
|
+
if (callback.body.statements.length !== 1 || !ts.isReturnStatement(callback.body.statements[0])) return undefined
|
|
1941
|
+
return callback.body.statements[0].expression
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
|
|
1945
|
+
for (const statement of sourceFile.statements) {
|
|
1946
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === identifier.text && statement.body) {
|
|
1947
|
+
const clone = cloneAst(statement, factory, context)
|
|
1948
|
+
return factory.createFunctionExpression(clone.modifiers?.filter(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword), clone.asteriskToken, clone.name, clone.typeParameters, clone.parameters, clone.type, clone.body)
|
|
1949
|
+
}
|
|
1950
|
+
if (!ts.isVariableStatement(statement)) continue
|
|
1951
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === identifier.text)
|
|
1952
|
+
if (declaration?.initializer && (ts.isArrowFunction(declaration.initializer) || ts.isFunctionExpression(declaration.initializer))) return cloneAst(declaration.initializer, factory, context)
|
|
1953
|
+
}
|
|
1954
|
+
return undefined
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function isPureReactMemoExpression(node) {
|
|
1958
|
+
node = unwrapExpression(node)
|
|
1959
|
+
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
|
|
1960
|
+
if (ts.isParenthesizedExpression(node)) return isPureReactMemoExpression(node.expression)
|
|
1961
|
+
if (ts.isPrefixUnaryExpression(node)) return ![ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator) && isPureReactMemoExpression(node.operand)
|
|
1962
|
+
if (ts.isBinaryExpression(node)) return node.operatorToken.kind < ts.SyntaxKind.FirstAssignment && isPureReactMemoExpression(node.left) && isPureReactMemoExpression(node.right)
|
|
1963
|
+
if (ts.isConditionalExpression(node)) return isPureReactMemoExpression(node.condition) && isPureReactMemoExpression(node.whenTrue) && isPureReactMemoExpression(node.whenFalse)
|
|
1964
|
+
if (ts.isTemplateExpression(node)) return node.templateSpans.every(span => isPureReactMemoExpression(span.expression))
|
|
1965
|
+
return false
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function reactMemoReferenceNames(root) {
|
|
1969
|
+
const names = new Set()
|
|
1970
|
+
const visit = node => {
|
|
1971
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node)) names.add(node.text)
|
|
1972
|
+
ts.forEachChild(node, visit)
|
|
1973
|
+
}
|
|
1974
|
+
visit(root)
|
|
1975
|
+
return names
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1747
1978
|
function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings, listExpressions, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, clientImports, workerReferences) {
|
|
1748
1979
|
return context => sourceFile => {
|
|
1749
1980
|
const factory = context.factory
|
|
1750
1981
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
1982
|
+
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
1983
|
+
ts.setParentRecursive(sourceFile, false)
|
|
1751
1984
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
1752
1985
|
ts.setParentRecursive(sourceFile, false)
|
|
1753
1986
|
rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
|
|
@@ -1757,7 +1990,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1757
1990
|
const importedSource = target => {
|
|
1758
1991
|
let imported = importedSources.get(target)
|
|
1759
1992
|
if (!imported) {
|
|
1760
|
-
imported =
|
|
1993
|
+
imported = normalizeReactMigrationSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
1994
|
+
ts.setParentRecursive(imported, false)
|
|
1995
|
+
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
1761
1996
|
ts.setParentRecursive(imported, false)
|
|
1762
1997
|
importedSources.set(target, imported)
|
|
1763
1998
|
}
|
|
@@ -3601,6 +3836,11 @@ function nearestFunction(node) {
|
|
|
3601
3836
|
return undefined
|
|
3602
3837
|
}
|
|
3603
3838
|
|
|
3839
|
+
function nearestFunctionLike(node) {
|
|
3840
|
+
for (let current = node.parent; current; current = current.parent) if (isFunctionLike(current)) return current
|
|
3841
|
+
return undefined
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3604
3844
|
function isShadowedByParameter(node, scopeRoot) {
|
|
3605
3845
|
for (let current = node.parent; current; current = current.parent) {
|
|
3606
3846
|
if (isFunctionLike(current) && current.parameters.some(parameter => bindingNames(parameter.name).includes(node.text))) return true
|
|
@@ -3616,6 +3856,7 @@ function isShadowedIdentifier(node, scopeRoot) {
|
|
|
3616
3856
|
if (isFunctionLike(scopeRoot) && functionVarDeclaresName(scopeRoot, node.text)) return true
|
|
3617
3857
|
for (let current = node.parent; current; current = current.parent) {
|
|
3618
3858
|
if (current === scopeRoot) break
|
|
3859
|
+
if (ts.isFunctionExpression(current) && current.name?.text === node.text) return true
|
|
3619
3860
|
if (ts.isBlock(current) && current.statements.some(statement => statementDeclaresName(statement, node.text))) return true
|
|
3620
3861
|
if (ts.isCaseBlock(current) && current.clauses.some(clause => clause.statements.some(statement => statementDeclaresName(statement, node.text)))) return true
|
|
3621
3862
|
if (ts.isCatchClause(current) && current.variableDeclaration && bindingNames(current.variableDeclaration.name).includes(node.text)) return true
|