0.0.32 • Published 4 months ago

battuta v0.0.32

Weekly downloads
-
License
MIT
Repository
-
Last release
4 months ago

Battuta

A experimental signals based /frontend/ framework allowing to use JSX with non-component objects\ 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

Example

Composing third party objects

import { time } from "battuta/utils/time";
import { createComputed } from "battuta/utils/signals";
import { Canvas } from "battuta/utils/three";
// import objects directly from third party libraries
import { Mesh, BoxGeometry, MeshBasicMaterial, Group, BufferGeometry } from "three";

function Rotated(geometry: BufferGeometry) {
    return geometry.rotateX(-Math.PI / 2);
}

function App() {
    const [ amplitude, setAmplitude ] = createSignal(10);
    const wave = createComputed(() => Math.sin(time()) * amplitude());
    const increase = () => setAmplitude(amplitude() + 1);

    return <div>
        <button onclick={increase}>Increase</button>
        <Canvas> {/* transition component, dom <=> three */}
            <Group> {/* compose third party objects */}
                <Mesh position:y={wave() + 5}> {/* assign direct or deep properties to reactive values */}
                    <BoxGeometry width={2}/> {/* set constructor arguments */}
                    <MeshBasicMaterial />
                </Mesh>
                <Mesh position:setX$={[wave()]}> {/* call member methods when the derived signal change */}
                    <Rotated> {/* Call non component functions using childs as arguments */}
                        <BoxGeometry width={2}/>
                    </Rotated>
                    <MeshBasicMaterial />
                </Mesh>
                <Mesh position:setX$={[wave()]}>
                    <Rotated geometry={<BoxGeometry width={2}/>}/> {/* Call non component functions using explicit arguments */}
                    <MeshBasicMaterial />
                </Mesh>
            </Group>
        </Canvas>
    </div>
}

The composition implementations can be customized per prototype see Customize

Macros

Macros are functions that run at build time and replace their own content (or more). Battuta includes a fork of unplugin-macros exposing more macros options.

import { css } from "battuta/macros/css.macro";
const styles = css`
    .myClass {
        color: red;
    }
`

Transforms to

import "./.temp/styles/PQOn.css"
const styles = { "myClass": ".cls-0o8g8wl1" }

You can write custom macros by naming them filename.macro.ts. Macros are exectued after JSX is transformed and as JSX compiles to direct functions, you can write JSX macros

import { readFileSync } from "fs";

export function Svg(args) {
    const svgText = readFileSync(args.url, "utf-8")
    const minified = minify(svgText);
    return new String(`(() => {
        let el = document.createElement("div");
        el.innerHTML = \`${minified}\`;
        return el.children[0]
    )()`);
}
import { Svg } from "./mymacro.macro"

function App() {

    return <Svg url="./icons/arrow.svg"/>
}

Macros can also query & manipulate the file's AST

import { MacroContext } from "battuta/macros";
export function contextMacro(args) {
    const ctx = this as MacroContext;
    const node = ctx.node;
    if(node.type !== "CallExpression") throw new Error("Invalid use of the macro")
    const contextFunction = node.arguments[1];
    const lastStatment = contextFunction.body.body[contextFunction.body.body.length - 1];
    ctx.magicString.overwriteNode(lastStatment, `10`)
}

JSX

JSX expressions are compiled differently based on the type of the tag used

DOM

This expression:

<div style:color={color()}>
    {value()}
</div>

Transforms to:

createElement("div")
    [assign](() => color(), "style", "color")
    [append](() => value())

Components

Given

function Component(props) {
    return <div>{props.value}</div>
}

This expression:

<Component value={value()}>

Transforms to:

Component({ get value() { return value() } })

Constructors

Given

class A {
    prop = 0;
    constructor(arg_2);
    constructor(arg_1, arg_2, arg_3) {}
}

Those expressions:

<A arg_2={value_2()} prop={value_3()}>
<A arg_2={value_2()} arg_3={value_4()} prop={value_3()}>

Transform to:

A[create](value_2())
    [assign](() => value_3(), "prop")
A[create](undefined, value_2(), value_4())
    [assign](() => value_3(), "prop")

The props names are matched with the args names

Functions

Given

function F(first: string, second: number) {}

Those expressions:

<F first={value_1()} second={value_2()}>
<F second={value_2()} prop={value_3()}>

Transform to:

F(value_1(), value_2())
F(undefined, value_2())
    [assign](() => value_3(), "prop")

Customize

for this to work some methods need to be implemented on the parent prototypes, by default Object instances define default implementations that can be overwritten, at least insert and remove need to be implemented

this is an example in the case of threejs

import { append, remove, childrenIndex, empty } from "battuta/runtime";
import { Object3D, Group } from "three";

// required 
Object3D.prototype[insert] = function(child: any, index?: number){
    this.add(child);
    return this;
}

// required 
Object3D.prototype[remove] = function(){
    this.removeFromParent();
    return this;
}

// not needed if the childrens order doesn't matter 
Object3D.prototype[childrenIndex] = function(child){
    return this.children.indexOf(child);
}

// not needed if the childrens order doesn't matter 
Object3D.prototype[empty] = function(){
    return new Group();
}

// implemented by default, can be overwritten
Object3D.prototype[create] = function (...props) {
    return Reflect.construct(this as any, props);
}

// implemented by default, can be overwritten
Object3D.prototype[set] = function (value, ...keys) {
    const key = keys.pop()!;
    resolveObj(this, keys)[key] = value;
    return this;
}

// implemented by default, can be overwritten
Object3D.prototype[assign] = function (value, ...keys) {
    const key = keys.pop()!;
    const target = resolveObj(this, keys);
    useEffect(() => target[key] = value());
    return this;
}

// implemented by default, can be overwritten
Object3D.prototype[call] = function (value, ...keys) {
    const key = keys.pop()!;
    const target = resolveObj(this, keys);
    const f = target[key].bind(target);
    useEffect(() => f(...value()));
    return this;
}

// could be overwritten for advance usages, see lib/runtime/index.ts
Object3D.prototype[append] // compose the tree
Object3D.prototype[cleanup] // handles element removals
Object3D.prototype[seal] // called once all props & childs have been collected

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/>

    return <EventsProvider>
        <Child/> {/* run inside the EventsProvider */}
        {child1} {/* run outside the EventsProvider */}
        {child2} {/* run inside the EventsProvider */}
    <EventsProvider>
}

Typescript

The framework uses features that are not available in typescript currently, for the following to work you need to install this package (build of this typescript fork) and set VSCode to use the workspace's typescript version

const a = <div/> // typed as HTMLDivElement
const b = <Array/> // typed as Array
const d = <F/> // typed as ReturnType<typeof F>, does not work if F is a Generic

const e = <>
    <div/>
    {10}
    {"10" as const}
</> // [HTMLDivElement, number, "10"]

const F = (arg: number, other_arg: string) => 10;

const f = <F
    other_arg={"text"}
    arg={"10"} // Type 'string' is not assignable to type 'number'
/>

const f = <F> {/* Type 'string' is not assignable to type 'number' */}
    {"10"}
    {"text"}
</F>

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: [
        // optional config
        battutaPlugin({
            // options for the bun macros plugin
            macros: Options,
            root: "src/main.tsx",
            optimizer: {
                strings: true,
                functions: true,
            },
        })
    ]
})

sources

JSX

the battutaJSX plugin handle the JSX transformations

import { battutaJSX, battutaInferModes } from "battuta/vite";

export default defineConfig({
    plugins: [
        battutaInferModes(),
        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>
`);
0.0.30

4 months ago

0.0.31

4 months ago

0.0.32

4 months ago

0.0.27

4 months ago

0.0.28

4 months ago

0.0.29

4 months ago

0.0.20

4 months ago

0.0.21

4 months ago

0.0.22

4 months ago

0.0.23

4 months ago

0.0.24

4 months ago

0.0.25

4 months ago

0.0.17

4 months ago

0.0.18

4 months ago

0.0.19

4 months ago

0.0.26

4 months ago

0.0.16

4 months ago

0.0.15-modeless6

4 months ago

0.0.15-modeless5

4 months ago

0.0.15-modeless8

4 months ago

0.0.15-modeless7

4 months ago

0.0.15-modeless4

4 months ago

0.0.15-modeless9

4 months ago

0.0.15-modeless10

4 months ago

0.0.15-modeless11

4 months ago

0.0.15-modeless3

4 months ago

0.0.15-modeless

5 months ago

0.0.12

5 months ago

0.0.13

5 months ago

0.0.14

5 months ago

0.0.15-modeless2

5 months ago

0.0.10

5 months ago

0.0.11

5 months ago

0.0.9

5 months ago

0.0.8

5 months ago

0.0.7

5 months ago

0.0.6

5 months ago

0.0.5

5 months ago

0.0.4

7 months ago

0.0.3

8 months ago

0.0.2

8 months ago

0.0.1

9 months ago

0.0.0

9 months ago