
Security News
TypeScript is Porting Its Compiler to Go for 10x Faster Builds
TypeScript is porting its compiler to Go, delivering 10x faster builds, lower memory usage, and improved editor performance for a smoother developer experience.
A experimental signals based /frontend/ framework allowing to use JSX with non-component objects\ highly inspired from [solidjs](https://www.solidjs.com/) and [solid-three](https://github.com/solidjs-community/solid-three)
A experimental signals based /frontend/ framework allowing to use JSX with non-component objects
highly inspired from solidjs and solid-three
npx battuta init
then run it with npm run dev
import { time } from "battuta/utils/time";
import { computed } 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 = computed(() => 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 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 expressions are compiled differently based on the type of the tag used
This expression:
<div style:color={color()}>
{value()}
</div>
Transforms to:
createElement("div")
[assign](() => color(), "style", "color")
[append](() => value())
Given
function Component(props) {
return <div>{props.value}</div>
}
This expression:
<Component value={value()}>
Transforms to:
Component({ get value() { return value() } })
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
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")
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
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>
}
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>
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>
As the framework is mostly a vite wrapper you can also use it with vite directly, or by part.
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,
},
})
]
})
the battutaJSX
plugin handle the JSX transformations
import { battutaJSX, battutaInferModes } from "battuta/vite";
export default defineConfig({
plugins: [
battutaInferModes(),
battutaJSX()
]
})
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
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()
]
})
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()
]
})
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()
]
})
the battutaConfig
export contain the default config plugin
import { battutaConfig } from "battuta/vite";
export default defineConfig({
plugins: [
battutaConfig()
]
})
CLI actions are also available from javascript
import { compile, transformJSX } from "battuta/compiler";
import { optimize, optimizeStrings } from "battuta/optimizer";
const { code } = transformJSX(`
<div></div>
`);
FAQs
A experimental signals based /frontend/ framework allowing to use JSX with non-component objects\ highly inspired from [solidjs](https://www.solidjs.com/) and [solid-three](https://github.com/solidjs-community/solid-three)
The npm package battuta receives a total of 15 weekly downloads. As such, battuta popularity was classified as not popular.
We found that battuta demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
TypeScript is porting its compiler to Go, delivering 10x faster builds, lower memory usage, and improved editor performance for a smoother developer experience.
Research
Security News
The Socket Research Team has discovered six new malicious npm packages linked to North Korea’s Lazarus Group, designed to steal credentials and deploy backdoors.
Security News
Socket CEO Feross Aboukhadijeh discusses the open web, open source security, and how Socket tackles software supply chain attacks on The Pair Program podcast.