Battuta
a sketchy experimental frontend framework
highly inspired from solidjs and solid-three
Setup a new project
Web: npx battuta init
then run it with npm run dev
Table of Contents
JSX
the JSX compiler use a mode system that change how jsx expressions are compiled.
to switch between modes you can use $
utils like this:
<$f> {}
<Component>
<Child/> {}
{() => <Child/>} {}
</Component>
</$f>
<Component $f>
<Child/> {/* affected */}
{() => <Child/>} {/* not affected */}
</Component>
I hate to have to do it this way but it's the best I found yet
Default Mode - $d
the default mode compile JSX expressions to match the react component apis, as solid it use accesors for props
<Component key1={value()} key2="value">
<div/>
<div/>
</Component>
const createElement = document.bind(document);
Component({
get key1() { return value() },
key2: "value",
children: () => [ createElement("div"), createElement("div") ]
})
Class Mode - $c
the class mode allow to use already existing classes in JSX and compose them
import { create, appendMultiple, set, assign, call } from "battuta/runtime"
const group = <THREE.Group $c>
<THREE.PointLight
$c={[0xffff00, 2, 100]}
color:set={$call(color())}
position:y={positionY()}
position:x={1}
castShadow
/>
</THREE.Group>
const group = THREE.Group[create]()
[appendMultiple](
THREE.PointLight[create](0xffff00, 2, 100)
[call](() => color(), "color", "set")
[assign](() => position(), "position", "y")
[set](1, "position", "x")
[set](true, "castShadow")
)
for this to work some methods need to be implmented on the parent prototypes, by default Object instances define default implementations that can be overwriten, at least empty
, childrenIndex
, insert
and remove
need to be implemented, those last two should never be called directly, instead use the useAppend
& useRemove
hooks or the append
and cleanup
methods
this is an example in the case of threejs
import { append, remove, childrenIndex, empty } from "battuta/runtime";
import { Object3D, Group } from "three";
Object3D.prototype[insert] = function(child: any, index?: number){
this.add(child);
return this;
}
Object3D.prototype[remove] = function(){
return this.removeFromParent();
}
Object3D.prototype[childrenIndex] = function(child){
return -1;
}
Object3D.prototype[empty] = function(){
return new Group();
}
Function Mode - $f
the function mode allow to compose functions with each other
const switchCase = <t.switchCase $f>
{value}
<array>
<t.returnStatement>
{result}
</t.returnStatement>
</array>
</t.switchCase>
const switchStatment = t.switchCase(value, [ t.returnStatment(result) ])
Mixed Mode - $n
the mixed mode is a mix between the class mode and the function mode
const boxMesh = <Mesh $n
castShadow
receiveShadow
>
<BoxGeometry/>
<MeshPhongMaterial
color:set={$call(color())}
/>
</Mesh>
const boxMesh = Mesh[create](
BoxGeometry[create](),
MeshPhongMaterial[create]()
[call](() => color(), "color", "set")
)
[set](true, "castShadow")
[set](true, "receiveShadow")
Contexts
like other frameworks it also support contexts
const [ useValue, ValueProvider ] = createContext((props) => {
const [ getValue, setValue ] = createSignal();
return {
getValue,
setValue,
}
})
function App() {
return <ValueProvider>
<Component/>
</ValueProvider>
}
const [ useEvents, EventsProvider ] = createContext(() => new EventTarget());
function Component() {
const { getValue, setValue } = useValue();
const child1 = <Child/>
const child2 = () => <Child/>
doSomething(<Child/>)
return <EventsProvider>
<Child/>
{child1}
{child2}
<EventsProvider>
}
Some Hooks
quick examples of some builtin hooks
function Component() {
const add = useAppend();
add(<Child />)
const remove = useRemove();
remove();
onCleanup(() => {
})
useEffect(() => {
reactive() + 1;
})
useDebounced(() => {
reactive() + 1;
}, 1000)
const { camera, scene } = useScene();
useFrame((delta) => {})
const [getValue, setValue] = createTweakSignal("#ffffff");
useMonitor(() => Date.now())
}
Even tho there's no virtual dom we still has virtual relations & contexts. For example in this situation
const Component = () => () => () => () => () => {
return <h1>Hello</h1>
}
const div = <div/>
the real tree looks like
- div
|---- h1
tho in relatity the relations look like this
- div
|---- f1
| |---- f2
| |---- f3
| |---- f4
| |---- f5
|-------------------|---- h1
the real tree is still composed just of the div and the h1 elements but the intermediary contexts exist (if consumed) so here each element are bound to their parent.
one thing to keep in mind is the virtual element exist only if the real elements received an uncalled function, this happens by default to components in the normal mode as their children prop is a function but for other modes it is not the case, so for example here
function Child() {
return <h1/>
}
function Parent() {
return <div>
<Child/>
</div>
}
here two uncommon things happen, first here's the relation map of this code
- Parent (virtual, assuming it was used in a default mode component)
|---- div
| |---- h1
|---- Child
You can see the the child function don't appear where it should, this happens because the compilation result here is
function Parent() {
return createElement(div)[append](Child())
}
the div only receive the result of the child which is the second div so it never knows about the Child context, this generally don't cause issues as the div don't hold any special context.
the second uncommon behavior is that if you have a onCleanup
hook in your Child the Child is gonna consume from the parenting context, which in this case is the Parent element, this doesn't cause issues unless you're directly removing the div separatly using the cleanup
method, in this case the div will be removed as well as the h1 but the onCleanup
hook won't run for the child as it's bound to the Parent component
CLI
battuta init
create a new empty project
battuta bundle
use vite to bundle the app
battuta dev
open the vite dev server
battuta compile <file>
transform the given file
battuta compile:jsx <file>
transform the given file's jsx expressions
battuta optimize <file>
run the optimization steps (no terser minification)
battuta optimize:strings <file>
battuta optimize:functions <file>
Use with vite
As the framework is mostly a vite wrapper you can also use it with vite directly, or by part.
Vite plugins
All in one plugin
the default export of battuta/vite
is a plugin containing all the plugins as well as the default vite config
import battutaPlugin from "battuta/vite";
export default defineConfig({
plugins: [
battutaPlugin({
macros: Options,
optimizer: {
strings: true,
functions: true,
},
})
]
})
sources
JSX
the battutaJSX
plugin handle the JSX transformations
import { battutaJSX } from "battuta/vite";
export default defineConfig({
plugins: [
battutaJSX()
]
})
sources
Optimizer
battuta mostly use the terser integration in vite to optimize and minify builds, the battutaOptimizer
act as a preparation step before terser to help improve the build.
import { battutaOptimizer } from "battuta/vite";
export default defineConfig({
plugins: [
battutaOptimizer({
strings: true,
functions: true,
})
]
})
the strings optimizer catch all string duplicates in the codebase (happens a lot with JSX) and merge them in const declarations which can be minified
the functions optimizer raise function definitions to the highest scope it can reach, for example
this piece of code:
function doSomething(arg) {
return [
() => args
.filter(x => x > 10)
.map(x => x ** 2)
.filter(x => x < 1000)
.forEach(x => console.log(x)),
() => false
]
}
becomes this:
const f1 = x => x > 10;
const f2 = x => x ** 2;
const f3 = x => x < 1000;
const f4 = x => console.log(x);
const f5 = () => false;
function doSomething(arg) {
return [
() => args
.filter(f1)
.map(f2)
.filter(f3)
.forEach(f4),
f5
]
}
I have no idea if this may break some libs or if it has any benefit, but I wanned to try that
sources
Macros
the battutaMacros
plugin is a simple fork of unplugin-macros exposing the AST to the macro and allowing it to inject raw js code. checkout the css macro for an example
import { battutaMacros } from "battuta/vite";
export default defineConfig({
plugins: [
battutaMacros()
]
})
sources
Virtual Root
the battutaVirtualRoot
plugin create the index.html file, for now it just allow to remove it from the repo
import { battutaVirtualRoot } from "battuta/vite";
export default defineConfig({
plugins: [
battutaVirtualRoot()
]
})
sources
Folders
the battutaFolders
plugin moves the content of the .temp
to the .dist
folder during the build
import { battutaFolders } from "battuta/vite";
export default defineConfig({
plugins: [
battutaFolders()
]
})
sources
Config
the battutaConfig
export contain the default config plugin
import { battutaConfig } from "battuta/vite";
export default defineConfig({
plugins: [
battutaConfig()
]
})
sources
Transformation APIs
CLI actions are also available from javascript
import { compile, transformJSX } from "battuta/compiler";
import { optimize, optimizeStrings } from "battuta/optimizer";
const { code } = transformJSX(`
<div></div>
`);