Node.js Integration in Lux
Mix.install([
{:lux, "~> 0.4.0"},
{:kino, "~> 0.14.2"}
])
Application.ensure_all_started([:lux])
Overview
Lux provides robust support for Node.js, allowing you to leverage the vast JavaScript ecosystem in your agents. This guide explains how to use Node.js effectively with Lux.
Writing JavaScript Code
Using the ~JS Sigil
The ~JS
sigil allows you to write JavaScript code directly in your Elixir files. Note that all JavaScript code must export a main
function that will be called by Lux:
defmodule MyApp.Prisms.TextProcessingPrism do
use Lux.Prism,
name: "Text Processing"
require Lux.NodeJS
import Lux.NodeJS
def handler(input, _ctx) do
result = nodejs variables: %{text: input} do
~JS"""
import { tokenize } from 'natural';
import { sentiment } from 'sentiment';
export const main = ({text}) => {
// Process input text
const tokens = tokenize(text);
const analysis = sentiment(text);
return {
tokens,
sentiment: analysis.score,
comparative: analysis.comparative
};
};
"""
end
{:ok, result}
end
end
Let’s try it with some simple text processing:
require Lux.NodeJS
import Lux.NodeJS
# First, import the natural language processing package
{:ok, %{"success" => true}} = Lux.NodeJS.import_package("natural")
# Now process some text
nodejs variables: %{text: "Hello, this is a test sentence!"} do
~JS"""
import { tokenize, PorterStemmer } from 'natural';
export const main = ({text}) => {
const tokens = tokenize(text);
const stems = tokens.map(token => PorterStemmer.stem(token));
return {
original: text,
tokens,
stems
};
};
"""
end
Key features:
- Modern JavaScript (ES modules) support
- Variable binding between Elixir and JavaScript
- Automatic type conversion
- Error handling and timeouts
- Full async/await support
Custom JavaScript Modules
You can add your own JavaScript modules under the priv/node
directory:
priv/node/
├── src/
│ ├── analysis.mjs
│ └── utils.mjs
├── package.json
└── package-lock.json
These modules can be imported and used in your Lux code:
nodejs do
~JS"""
import { analyzeText } from './src/analysis.mjs';
import { formatOutput } from './src/utils.mjs';
export const main = async () => {
const result = await analyzeText(input);
return formatOutput(result);
};
"""
end
Package Management
Using NPM
Lux uses NPM for JavaScript package management. The package.json
file in priv/node
defines your dependencies:
{
"name": "lux-nodejs",
"version": "0.1.0",
"description": "Node.js support for Lux framework",
"type": "module",
"dependencies": {
"natural": "^6.0.0",
"sentiment": "^5.0.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"jest": "^29.0.0",
"eslint": "^8.0.0"
}
}
To install dependencies:
cd priv/node
npm install
Importing Packages
Use Lux.NodeJS.import_package/1
to dynamically import Node.js packages:
# Import lodash for data manipulation
{:ok, %{"success" => true}} = Lux.NodeJS.import_package("lodash")
# Let's use it to process some data
nodejs variables: %{data: [1, 2, 3, 4, 5]} do
~JS"""
import _ from 'lodash';
export const main = ({data}) => {
return {
sum: _.sum(data),
mean: _.mean(data),
chunks: _.chunk(data, 2)
};
};
"""
end
Type Conversion
Lux automatically handles type conversion between Elixir and JavaScript:
Elixir Type | JavaScript Type |
---|---|
nil |
null |
true /false |
true /false |
Integer |
number |
Float |
number |
String |
string |
List |
Array |
Map |
Object |
Struct | JavaScript class |
Let’s see type conversion in action:
nodejs variables: %{
number: 42,
text: "hello",
list: [1, 2, 3],
map: %{key: "value"}
} do
~JS"""
export const main = (vars) => ({
numberType: typeof vars.number,
textType: typeof vars.text,
listType: Array.isArray(vars.list),
mapType: typeof vars.map,
// Show some conversions
conversions: {
nullToNil: null,
boolToAtom: true,
intToNumber: 42,
arrayToList: [1, "two", 3.0]
}
});
"""
end
Async/Await Support
Lux fully supports JavaScript’s async/await. The main
function can be async:
# Import axios for HTTP requests
{:ok, %{"success" => true}} = Lux.NodeJS.import_package("axios")
nodejs do
~JS"""
import axios from 'axios';
export const main = async () => {
try {
const response = await axios.get('https://api.github.com/users/octocat');
return {
success: true,
data: {
login: response.data.login,
name: response.data.name,
repos: response.data.public_repos
}
};
} catch (error) {
return {
success: false,
error: error.message
};
}
};
"""
end
Error Handling
JavaScript errors are converted to Elixir exceptions. You have several options for handling them:
1. Handle Errors in JavaScript
# Handle errors in the JavaScript code
nodejs do
~JS"""
export const main = () => {
try {
// This will throw a ReferenceError
const result = undefinedVariable;
return { success: true, result };
} catch (error) {
return {
success: false,
error: error.message,
type: error.constructor.name
};
}
};
"""
end
2. Handle Exceptions in Elixir
# Use try/rescue in Elixir
try do
nodejs! do
~JS"""
export const main = () => {
// This will throw an Error
throw new Error("Something went wrong");
};
"""
end
rescue
NodeJS.Error -> "Caught JavaScript error"
end
3. Pattern Match on Results
# Use pattern matching with nodejs/2
case nodejs do
~JS"""
export const main = () => {
try {
const result = JSON.parse('invalid json');
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
};
"""
end do
{:ok, %{"success" => true, "data" => data}} ->
"Got data: #{inspect(data)}"
{:ok, %{"success" => false, "error" => error}} ->
"Got error: #{error}"
{:error, error} ->
"JavaScript execution failed: #{error}"
end
Testing
Test your JavaScript code using the standard Elixir testing tools:
defmodule MyApp.Prisms.TextProcessingPrismTest do
use UnitCase, async: true
import Lux.NodeJS
test "processes text correctly" do
{:ok, %{"success" => true}} = Lux.NodeJS.import_package("natural")
result = nodejs variables: %{text: "Hello, world!"} do
~JS"""
import { tokenize } from 'natural';
export const main = ({text}) => {
return tokenize(text);
};
"""
end
assert {:ok, ["Hello", "world"]} = result
end
test "handles errors gracefully" do
result = nodejs do
~JS"""
export const main = () => {
try {
undefinedVariable;
} catch (error) {
return {
status: "error",
message: "Variable not defined"
};
}
};
"""
end
assert {:ok, %{"status" => "error"}} = result
end
end
Best Practices
-
Module Organization
-
Keep related JavaScript code in modules under
priv/node/src
- Use ES modules for better code organization
- Follow JavaScript style guidelines
-
Keep related JavaScript code in modules under
-
Performance
- Use async/await for I/O operations
- Batch operations to minimize cross-language calls
- Consider memory usage with large datasets
-
Error Handling
- Handle expected errors in JavaScript with try/catch
- Use pattern matching in Elixir for high-level flow control
- Provide meaningful error messages
- Clean up resources in error cases
-
Testing
- Test both success and error cases
- Verify type conversions
- Test with realistic data
Coming Soon
Lux will soon support defining components entirely in JavaScript:
import { Prism, Beam, Agent } from 'lux';
class MyPrism extends Prism {
name = "JavaScript Prism";
description = "A prism implemented in JavaScript";
async handler(input, context) {
try {
// Process input
const result = await this.processData(input);
return { success: true, data: result };
} catch (error) {
return { success: false, error: error.message };
}
}
}
This will allow you to:
- Write agents entirely in JavaScript
- Define prisms and beams in JavaScript
- Use JavaScript’s class system
- Leverage Node.js’s async capabilities
Stay tuned for updates!