Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 72 additions & 42 deletions source/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,52 +6,38 @@ interface ToModifyVariableI {
}

export default function () {
const toMod: ToModifyVariableI[] = []
let toMod: ToModifyVariableI[] = []
return {
visitor: {
FunctionDeclaration(path: any) {
transformToStateByScope(path, toMod)
VariableDeclaration(path: any) {
toMod = toMod.concat(getReactiveVariablesFromScope(path.scope))
const {node}: {node: t.VariableDeclaration} = path
transformReactiveDeclarations(node, toMod, path)
},
Identifier(path: any) {
const {node}: {node: t.Identifier} = path
if (
!t.isUpdateExpression(path.parentPath) &&
!t.isAssignmentExpression(path.parentPath) &&
!t.isCallExpression(path.parentPath)
) {
if (isReactiveIdentifier(node.name, toMod)) {
path.replaceWith(getStateTuple(node.name))
}
}
},
ExpressionStatement({node}: {node: t.ExpressionStatement}) {
transformAssignmentExpression(node, toMod)
},
ArrowFunctionExpression(path: any) {
transformToStateByScope(path, toMod)
CallExpression(path: any) {
if (isReadingReactiveValue(path.node, toMod)) {
path.replaceWith(getNormalIdentifierFromCall(path.node))
}
},
},
}
}

function transformToStateByScope(path: any, toMod: ToModifyVariableI[]) {
// TODO: check if the returned values are of the form `React.createElement`
// NOTE: can avoid the above one since custom hooks won't be able to use this

Object.keys(path.scope.bindings).forEach((binding) => {
if (/^\$/.test(binding)) {
// add to list of identifiers to compare and replace
// (not using scope replace to avoid shadow variables being replaced)
const normName = normalizeName(binding)
toMod.push({
raw: binding,
simplified: normName,
})
}
})

// nested traverse to avoid replacing bindings of anything other than what's in this
// function. To prevent creating state hooks outside a function
path.traverse({
Identifier({node}: {node: t.Identifier}) {
if (isReactiveIdentifier(node.name, toMod)) {
node.name = normalizeName(node.name)
}
},
VariableDeclaration({node}: {node: t.VariableDeclaration}) {
transformReactiveDeclarations(node, toMod, path)
},
ExpressionStatement({node}: {node: t.ExpressionStatement}) {
transformAssignmentExpression(node, toMod)
},
})
}

function transformReactiveDeclarations(
node: t.VariableDeclaration,
toMod: ToModifyVariableI[],
Expand Down Expand Up @@ -85,7 +71,7 @@ function transformReactiveDeclarations(
)

// fallback to replace missed instances of the variable
path.scope.rename(declaration.id.name, normName)
// path.scope.rename(declaration.id.name, normName)
}
}

Expand Down Expand Up @@ -155,9 +141,7 @@ function transformAssignmentExpression(
}

function isReactiveIdentifier(idName: string, modMap: ToModifyVariableI[]) {
return (
modMap.findIndex((x) => x.raw === idName || x.simplified === idName) > -1
)
return modMap.findIndex((x) => x.raw === idName) > -1
}

function getSetterName(normalizedName: string) {
Expand All @@ -169,3 +153,49 @@ function getSetterName(normalizedName: string) {
function normalizeName(n: string) {
return n.replace(/\$/, '')
}

function getStateTuple(reactiveVaribleName: string) {
const name = normalizeName(reactiveVaribleName)
const setter = getSetterName(name)
return t.arrayExpression([t.identifier(name), t.identifier(setter)])
}

function getNormalIdentifierFromCall(node: t.CallExpression) {
if (!t.isIdentifier(node.callee)) {
return
}

const name = normalizeName(node.callee.name)
return t.identifier(name)
}

function isReadingReactiveValue(
node: t.CallExpression,
modMap: ToModifyVariableI[]
) {
if (
!(
t.isIdentifier(node.callee) &&
isReactiveIdentifier(node.callee.name, modMap)
)
) {
return false
}
return true
}

function getReactiveVariablesFromScope(scope: any) {
const toMod: ToModifyVariableI[] = []
Object.keys(scope.bindings).forEach((binding) => {
if (/^\$/.test(binding)) {
// add to list of identifiers to compare and replace
// (not using scope replace to avoid shadow variables being replaced)
const normName = normalizeName(binding)
toMod.push({
raw: binding,
simplified: normName,
})
}
})
return toMod
}
34 changes: 34 additions & 0 deletions test/snapshots/test.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,3 +200,37 @@ Generated by [AVA](https://avajs.dev).
onClick: updateUser␊
}, "Click Me"));␊
}`

## state passed around functions

> Snapshot 1

`function useCustomHook() {␊
const [x, setX] = React.useState({␊
name: "reaper"␊
});␊
const addAge = () => {␊
setX({ ...x,␊
age: 18␊
});␊
};␊
return [...[x, setX], addAge];␊
}␊
const Component = () => {␊
let [x, setX, addAge] = useCustomHook();␊
const updateName = () => {␊
setX({ ...x,␊
name: "name"␊
});␊
};␊
return /*#__PURE__*/React.createElement(React.Fragment, null, x.name, x.age, /*#__PURE__*/React.createElement("button", {␊
onClick: updateName␊
}, "update"), /*#__PURE__*/React.createElement("button", {␊
onClick: addAge␊
}, "addAge"));␊
};`
Binary file modified test/snapshots/test.ts.snap
Binary file not shown.
72 changes: 64 additions & 8 deletions test/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const compile = (code: string) =>
plugins: [plugin],
})

test('Simple Transform', (t) => {
test.skip('Simple Transform', (t) => {
const code = `
import * as React from "react"

Expand All @@ -33,7 +33,7 @@ test('Simple Transform', (t) => {
t.snapshot(result.code)
})

test('Check Functional Scope', (t) => {
test.skip('Check Functional Scope', (t) => {
const code = `
import * as React from "react"

Expand All @@ -60,7 +60,7 @@ test('Check Functional Scope', (t) => {
t.snapshot(result.code)
})

test('Check Arrow Function Scope', (t) => {
test.skip('Check Arrow Function Scope', (t) => {
const code = `
import * as React from "react";

Expand Down Expand Up @@ -90,7 +90,7 @@ test('Check Arrow Function Scope', (t) => {
t.snapshot(result.code)
})

test('Multi Component Scope', (t) => {
test.skip('Multi Component Scope', (t) => {
const code = `
import * as React from "react";

Expand Down Expand Up @@ -136,7 +136,7 @@ test('Multi Component Scope', (t) => {
t.snapshot(result.code)
})

test('Hook Function and useEffect dep', (t) => {
test.skip('Hook Function and useEffect dep', (t) => {
const code = `
import * as React from "react";

Expand Down Expand Up @@ -167,7 +167,7 @@ test('Hook Function and useEffect dep', (t) => {
t.snapshot(result.code)
})

test('Singular Binary Expressions', (t) => {
test.skip('Singular Binary Expressions', (t) => {
const code = `
import React from "react";

Expand All @@ -194,7 +194,7 @@ test('Singular Binary Expressions', (t) => {
t.snapshot(result.code)
})

test('Object Update', (t) => {
test.skip('Object Update', (t) => {
const code = `
import * as React from "react";

Expand Down Expand Up @@ -223,7 +223,7 @@ test('Object Update', (t) => {
t.snapshot(result.code)
})

test('Array Update', (t) => {
test.skip('Array Update', (t) => {
const code = `
import * as React from "react";

Expand Down Expand Up @@ -253,3 +253,59 @@ test('Array Update', (t) => {
}
t.snapshot(result.code)
})

test('state passed around functions', (t) => {
const code = `
function useCustomHook(){
let $x = {name:"reaper"};

const addAge = () => {
$x = {
...$x(),
age:18
}
}
return [...$x,addAge];
}

const Component = () => {
let [x, setX, addAge] = useCustomHook();
const updateName = () => {
setX({
...x,
name: "name",
});
};
return (
<>
{x.name}
{x.age}
<button onClick={updateName}>update</button>
<button onClick={addAge}>addAge</button>
</>
);
};
`

const result = compile(code)
if (!result) {
return t.fail()
}
t.snapshot(result.code)
})

test.skip('Read executed state value', (t) => {
const code = `
function Component(){
let $x = 1;
console.log($x())
return <></>
}
`

const result = compile(code)
if (!result) {
return t.fail()
}
console.log(result.code)
})