
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
@cilix/lightjs
Advanced tools
Light.js is a JavaScript framework in the form of a compiler. Unlike other compiler-oriented JavaScript frameworks, however, Light is used like a proper compiler. It takes .light files, which look like this:
html [
head []
body [
h1 [ 'Welcome' ]
]
]
And compiles them into a single HTML file like this:
<doctype html>
<html>
<head></head>
<body>
<h1>Hello</html>
</body>
</html>
Using this command:
light index.light -o index.html
Or you can use the compiler programatically:
import { light } from 'lightjs';
const html = await light.compile({
input: 'index.light'
});
Light.js has components. Here is a counter in Light:
tag Counter [
let count = 0
button on:click='count++' [ 'increment' ]
div [ 'current count: {{count}}' ]
]
html [
head []
body [
Counter[]
]
]
Light.js is alpha software. This documentation is a work in progress, but it is comprehensive, and can be given directly to LLMs.
Create a project boilerplate using Light.js's built-in backend router. Usage below:
npx lightjs create my-app
Light.js syntax is extremely terse but it is not whitespace sensitive. It's a much less shift-y version of HTML. You simply drop the angle brackets, and put children between square brackets. Tags without children as well as void tags are ended with an empty set of square brackets.
form method='POST' action='/user' [
input name='first' []
input name='last' []
input type='submit' value='submit' []
]
Textnodes go inside strings.
a href='/' [ 'home' ]
If a textnode is the only child, the brackets may be omitted.
a href='/' 'home'
Remember, there is no whitespace sensitivity, so the following is equally valid.
a href='/'
'home'
Light.js supports class shorthand, as seen in other templating languages.
a.btn href='/' 'home'
Of course, you can use the regular class attribute.
a class='btn' href='/' 'home'
Light.js supports double-quoted string, single-quoted strings, and backtick strings. All strings may have interpolations.
html [
head []
body [
p `
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec fermentum arcu.
Aliquam vel ullamcorper ipsum. Nullam euismod nisl vel a tristique odio luctus.
Integer dapibus nec odio at lacinia. Pellentesque euismod id odio nec hendrerit.
`
]
]
Backtick strings give you total whitespace control when using them with whitespace sensitive tags like code or pre:
html [
head []
body [
code `
const foo = require('foo')
foo()
`
]
]
This prints the following block:
const foo = require('foo')
foo()
What's happening here is that the first line is being ignored because it contains only a newline. And the second backtick, the one on the bottom, is controlling how much whitespace is to the left of the code. Look at the next example where the bottom backtick is moved over two spaces:
html [
head []
body [
code `
const foo = require('foo')
foo()
`
]
]
This produces the following block:
const foo = require('foo')
foo()
A Light.js document is complete and doesn't require some sort HTML bootloader file. The following is a complete program.
html [
head []
body [
h1 "Hello from Light.js"
]
]
What follows is not Javascript. It is purely static Light.js data declaration. This is one of the key differences between Light.js and the rest. Light.js's opinion is that the document data, (what React calls "state"), belongs to the markup, and not the Javascript. Light.js sees Javascript as an extension that allows for interactivity.
let name = 'Matt'
html [
head []
body [
h1 "Hello, {{name}}"
]
]
Light.js supports strings, numbers, arrays, objects, boolean, and null, just like JSON.
let colors = [
'red',
'blue',
'green'
]
let person = {
name: 'Matt',
age: 33
}
html [
head []
body [
h1 "Hello, {{person.name}}"
]
]
Light.js supports the following operators:
+ - / * % ?:
let count = 4 * (2 + 3)
let many = count > 10 ? true : false
Arrays and objects can be accessed using expressions, as you might expect:
let person = {
name: 'Matt',
age: 33
}
let age = person.age
let name = person['nam' + 'e']
Expressions can be interpolated into strings using double curly braces.
let count = 4 * (2 + 3)
html [
head []
body [
p '{{ count > 10 ? "many" : "not many" }}'
]
]
Expressions can be passed into attributes using a single set of curly braces.
let link = '/'
let text = 'home'
html [
head []
body [
a href={link} '{{text}}'
]
]
Once a Light.js variable is declared it cannot be reassigned, except using Javascript (more on that later). The following is not valid.
let name = 'Matt'
name = 'Bob' // this is illegal
html [
head []
body [
h1 'Hello {{name}}'
]
]
For data transformations, Light.js has the pipe operator |:
let name = 'matt' | toupper
let nums = 1 | repeat 10 // creates an array of 10 1s
let length = nums | length // 10
html [
head []
body [
h1 'Hello {{name}}' // Hello MATT
]
]
toupper - Makes a string uppercase
let name = 'hello' | toupper
tolower - Makes a string lowercase
let name = 'HELLO' | tolower
length - Returns the length of the input
let len = x | length
repeat n - Repeats the input N times. All data types are supported, and will be repeated. Even large complex objects.
let nums = 1 | repeat 10 // 10 1s
let x = {} | repeat 100 // 100 empty objects
map x - Maps an array to an expression. The expression has 2 implicit arguments: _a, and _b which represent the value and the index respectively. This idea was borrowed from the Lobster programming language.
let people = [{
name: 'Frodo'
}, {
name: 'Sam'
}]
let names = people | map _a.name
// ['Frodo', 'Sam']
Here is an example of map being used in conjunction with repeat:
let nums = 1 | repeat 5 | map _a + _b
// [1, 2, 3, 4, 5]
The equivalent expression in javascript would look like this:
let nums = (new Array(5)).fill(1).map((_a, _b) => _a + _b);
filter x - Filters an array against an expression that returns true or false. The expression has 2 implicit arguments: _a, and _b which represent the value and the index respectively.
let nums = 0 | repeat 10 | map _a + _b | filter _b % 2 == 0
// [0, 2, 4, 6, 8]
split x - Splits a string at a given delimeter into an array
let greet = 'helloxworld' | split 'x'
// ['hello', 'world']
includes x - Checks whether or not an item is included in an array
let has4 = [1, 2, 3, 4, 5] | includes 4
// true
indexof x - Retrieves the index of an item in an array. Returns -1 if not found
let pos = [1, 2, 3, 4, 5] | indexof 4
// 3
reverse - Reverses a string or array
let nums = [1, 2, 3] | reverse
// [3, 2, 1]
let hello = 'hello' | reverse
// olleh
tostring - Converts any data type to a string, including arrays and objects
let person = { name: "matt" } | tostring
// '{"name":"matt"}'
let num = 10 | tostring
// '10'
todata - Converts a string of data into useable data
let person = '{"name":"matt"}' | todata
let number = '10' | todata
let thetruth = 'true' | todata
html [
head[]
body[
h1 'Hello {{person.name}}'
]
]
replace x y - Replaces the first instance of X in a string with Y
let str = 'hello world' | replace 'd' 'd!'
// 'hello world!'
join x - Joins an array into a string using a delimeter
let x = ['hello', 'world'] | join ' '
// 'hello world'
keys - Create an array from the keys of an object
let person = {
name: 'Frodo',
age: 80
}
let keys = person | keys
// ['name', 'age']
values - Create an array from the values of an object
let person = {
name: 'Frodo',
age: 80
}
let vals = person | values
// ['Frodo', 80]
trim - Trim the whitespace off the end of a string
let str = ' Hello world ' | trim
// 'Hello world'
slice x y - Create a slice from an array or string
let str = 'Hello world' | slice 0 5
// 'Hello'
rand x - Create a random floating point value between 0 and x
let val = 100 | rand
// 85.312
Light.js supports the following math pipes: ceil, floor, sin, cos, tan, sqrt
let v0 = 1 | sin
let v1 = 1 | cos
let v2 = 1 | tan
let v3 = 100 | sqrt
let v4 = 3.5 | ceil
let v5 = 4.5 | floor
let v6 = 100 | rand | floor
// In javascript, this would be equivalent to
// Math.floor(Math.random() * 100)
In Light.js, there are 3 control flow keywords: if, else, each
If statements evaluate a single expression. Brackets are used for control flow just like tag children.
let num = 10
html [
head []
body [
if (num > 10) [
p 'Number is greater than 10'
] else if (num > 5) [
p 'Number is greater than 5'
] else [
p 'Number is less than or equal to 5'
]
]
]
Each statements iterate over arrays and objects.
let num = [1, 2, 3, 4, 5]
html [
head []
body [
// the i is optional, and is the index of the item
each (n, i in nums) [
p 'value: {{n}}'
p 'index: {{i}}'
]
]
]
With objects, the first and second variables of the each statement are the key and value respectively:
let person = {
name: 'Frodo',
age: 80,
location: 'Shire'
}
html [
head []
body [
each (k, v in person) [
p '{{k}}: {{v}}'
]
]
]
You create components in Light.js with the tag keyword. Data that is local to a component must be declared within the component.
// a component with a single button
tag MyButton [
let text = 'click'
button.btn '{{text}}'
]
html [
head[]
body[
// use like any tag
MyButton[]
]
]
Components do not need to have a root element, and can simply be a list of elements:
tag MyButtonList [
let text = 'click'
button.btn '{{text}}'
button.btn '{{text}}'
button.btn '{{text}}'
]
html [
head[]
body[
MyButtonList[]
]
]
Components can be moved to separate files at your discretion. You are free to have single file components or multi-component files. Components in separate files can be imported using the import keyword and exported using the export keyword:
// buttons.lightjs
export tag BlueButton [
let text = 'click'
button.btn-blue '{{text}}'
]
export tag GreenButton [
let text = 'click'
button.btn-green '{{text}}'
]
// index.lightjs
// all exports are visible to this file
import 'buttons.lightjs'
html [
head[]
body[
BlueButton[]
GreenButton[]
]
]
While simple, this import strategy may cause a name collision. To handle this, an imported module may be namespaced using the 'as' keyword:
// index.lightjs
import 'buttons.lightjs' as btn
html [
head[]
body[
btn::BlueButton[]
btn::GreenButton[]
]
]
Component props can be accessed using the props keyword. Props are passed to components as regular attributes:
tag MyButton [
button.btn '{{props.text}}'
]
html [
head []
body [
MyButton text='click' []
]
]
Components can have children, and are displayed using the yield keyword:
tag MySection [
div.section-div [
yield
]
]
html [
head []
body [
MySection [
p [
'I am a child of .section-div'
]
]
]
]
Using yield is how common page layouts are achieved in Light.js:
// layout.lightjs
export tag Layout [
html lang='en' [
head [
title '{{props.title}}'
]
body [
yield
]
]
]
// index.lightjs
import 'layout.lightjs'
Layout title='My Blog' [
main [
h1 "Welcome!"
]
]
Light.js documents can be made interactive by adding Javascript. This requires no additional setup. All Javascript in Light.js goes between sets of dashes ---, or in event listener strings. The on: directive is used to attach event listeners to elements.
tag Counter [
// remember, this is not Javascript
let count = 0
// this part is Javascript
// Javascript in Light.js has the power to update template data directly
---
function increment() {
count++
}
---
// any valid browser event can follow the on: directive
// the string following 'on:click' is also javascript
button on:click='increment()' 'click count: {{count}}'
]
html [
head []
body [
Counter[]
]
]
From within Javascript, there are 3 functions and 1 object to know about: $on, $emit, $sync and $e. And that's all. The Javascript experience in Light.js doesn't require complex concepts and implicit contexts to learn about. It is simply contextualized properly so that you have access to your Light.js data and the ability to update the document.
$sync()This is the most important function call in your Javascript. This call updates the UI when your data changes. This call is analgous to setState in React, except that it's synchronous and takes no arguments. $sync() does NOT need to be called after event listeners, because it is called automatically.
// in this example, a counter is incremented every second
let count = 0
---
setInterval(() => {
count++
// sync needs to be called because this was not triggered by an event listener
$sync()
}, 1000)
---
html [
head []
body [
h1 "Count: {{count}}"
]
]
$sync() does not need to be called in the following example because increment() was called in an event listener.
tag Counter [
let count = 0
---
function increment() {
count++
}
---
button on:click='increment()' 'click count: {{count}}'
]
html [
head[]
body[
Counter[]
]
]
$eThis represents the event object within an event listener. There is nothing else to know about it. It is only available from within the event listener string.
let name = 'Matt'
---
function setInput(e) {
name = e.target.value
}
---
html [
head []
body [
input on:input='setInput($e)' []
h1 'Hello {{name}}'
]
]
$on(event, callback)This function call allows you to listen for lifecycle events within components. The current list of events is: mount, unmount, change, render.
tag MyComponent [
---
$on('mount', () => {
console.log('mounted')
})
$on('unmount', () => {
console.log('unmounted')
})
$on('change', (oldProps) => {
console.log('props are different than last time')
})
$on('prerender', () => {
console.log('right before rendering')
})
$on('render', () => {
console.log('finished rendering')
})
---
]
html [
head []
body [
MyComponent[]
]
]
$emit(event, data)This function call allows components to emit events that parent components can listen for.
tag Child [
---
function myClick(e) {
// emit a synthetic click event, and pass the object along
$emit('myclick', e)
}
---
button on:click='myClick($e)' 'click'
]
tag Parent [
// You simply use the on: directive on a component to listen for synthetic events
Child on:myclick='alert($e)' []
]
html [
head []
body [
Parent[]
]
]
lightjs.compile()Fundamentally, Light.js is a library that takes in a Light.js source tree and outputs a single HTML file or string.
const lightjs = require('lightjs');
lightjs.compile({
input: 'src/index.lightjs',
output: 'public/index.html'
});
No other step is required for a fully reactive, bundled, minified application.
This function may also be used to serve dynamic content in real time.
const express = require('express');
const lightjs = require('lightjs');
const app = express();
app.get('/', async (req, res) => {
const html = await lightjs.compile({
input: 'src/index.lightjs'
});
res.end(html);
});
app.listen(3838);
In order for the above example to have production quality speed, you must enable caching. And if desired, Javascript minification can be enabled as well:
const html = await lightjs.compile({
input: 'src/index.lightjs',
cache: true,
minify: true
});
The last thing to know about Light.js's compile function is document data.
const html = await lightjs.compile({
input: 'src/about.lightjs',
data: {
pageTitle: 'About me'
}
});
In your Light.js document pageTitle can be found on the global 'data' object:
// index.lightjs
html [
head []
body [
h1 '{{data.pageTitle}}'
]
]
lightjs.app()As mentioned above, Light.js ships with a built-in backend router. This router is exposed through the 'app' function, which returns a Node.js HTTP request handler.
const http = require('http');
const light = require('lightjs');
const PORT = 4477;
const app = light.router({
publicDir: './public',
cache: true,
minify: true,
routes: {
// map a route directly to a light file
'/': 'src/index.light',
// or for more complex arrangements, you can map the route to the same arguments you would pass to light.compile()
'/blog/:id': {
input: 'src/blog.light',
data: async (req) => {
const post = 'hi' // get blog post with req.params.id
return post
}
}
}
})
http.createServer(app).listen(PORT);
Light.js's router is also completely compatible with Express:
const http = require('http');
const light = require('lightjs');
const express = require('express');
const PORT = 4477;
const app = express();
const lightRouter = light.router({
cache: true,
minify: true,
routes: {
// ...
}
})
app.use(express.static('public'));
app.use(lightRouter);
http.createServer(app).listen(PORT);
Or, as previously mentioned, you can simply use light.compile with Express:
const http = require('http');
const light = require('lightjs');
const express = require('express');
const PORT = 4477;
const app = express();
app.get('/', async (req, res) => {
const html = await light.compile({
input: 'src/index.light',
// for production
minify: true,
cache: true
});
res.end(html);
});
http.createServer(app).listen(PORT);
This is the entire feature set of Lightjs. If there is even one aspect of Lightjs that is missing from this document, it is considered a bug.
FAQs
A new kind of JavaScript framework
We found that @cilix/lightjs demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer 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.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.