New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@cilix/lightjs

Package Overview
Dependencies
Maintainers
1
Versions
21
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cilix/lightjs

A new kind of JavaScript framework

latest
Source
npmnpm
Version
0.0.21
Version published
Maintainers
1
Created
Source

Light.js

A new kind of JavaScript framework

What is this?

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.

Table of Contents

  • Quick Start
  • Usage
  • Syntax
  • Document
  • Data
  • Control Flow
  • Components
  • Javascript

Quick Start

Create a project boilerplate using Light.js's built-in backend router. Usage below:

npx lightjs create my-app

Syntax

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()

Document

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"
  ]
]

Data

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

Built-in Pipes

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

Math Pipes

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)

Control Flow

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}}'
    ]
  ]
]

Components

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!"
  ]
]

Javascript

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[]
  ]
]

$e

This 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[]
  ]
]

Usage

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

Package last updated on 21 Sep 2025

Did you know?

Socket

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.

Install

Related posts