lex-press
v3.1.3
Published
Minimalist framework for HTML and JSX with LEX, built as an Express wrapper with automatic routing and server-side props
Readme
Lex-Press
This is my framework for working with HTML or JSX files with LEX. It follows a minimalist philosophy with full user control. It integrates utility functionalities from frameworks like Next.js but integrated into an Express wrapper.
Note: This framework includes body-parser and cookie-parser configured by default to facilitate request and cookie handling.
Installation
Use npm i lex-press or npx create-lex-press-app
The second option simply adds a small initial configuration.
Initialization
To get started, import lex-press with require and then instantiate an app just like you would in Express:
const lexpress = require("lex-press");
const app = lexpress();Now you'll have access to all Express methods since this framework only extends Express:
const lexpress = require("lex-press");
const app = lexpress();
app.get("/", (req, res) => res.send("Hello world"));
app.listen(3000, () => {
console.log("Listening on port 3000");
});App Tag
The lexpress factory accepts an optional tag parameter to identify the app instance. This is especially useful for libraries that bundle a lexpress app, making them resilient to async or non-deterministic mounting scenarios:
const app = lexpress({ tag: "my-library-app" });Without a tag, apps are identified by an auto-incrementing counter. When building for production, each tag gets its own folder structure under .lex-press-app/{tag}/.
Public
It's a utility method for serving public directories. You pass it the folder path and you're done:
app.public("public");Using a custom key
If your path depends on __dirname (e.g., in a library), you should provide a key option. This ensures the path mapping works correctly after bundling:
app.public(__dirname + "/public", { key: "my-library-public" });Without a key, the path is normalized relative to process.cwd(), which may not work correctly when bundled.
HTML
It's a method that declares a route and an HTML file to be served. In development and build mode, it compiles the HTML before serving it. In production, it expects the build to have already been done:
app.html("/home", "./pages/index.html");If the HTML includes src or inline scripts, it will be automatically compiled resulting in a single HTML with inline scripts.
The JavaScript bundle supports imports of CSS or module.css files, text files, JSON, and files. So you can import images and get a src. CSS is processed and added to the HTML files inline.
JSX
Similar to app.html but receives route, page, and layout:
app.jsx("/home", "./pages/layout.jsx", "./pages/page.jsx");Layout must export a Layout component and Page must export a Page component:
const Layout = ({ children }) =>
{
return <html>
<head>
<title>My App</title>
</head>
<body>
{children}
</body>
</html>;
};
export default Layout;const Page = () =>
{
return <h1>Hello world</h1>;
};
export default Page;Views
The views method declares an automatic routing folder in the style of Vite or Next.js:
app.views("./views");views/
├── layout.jsx
├── home/
│ └── page.jsx
├── about/
│ └── page.html
└── contact/
└── page.jsxA schema like this would serve the page elements.
- If we have
page.html, it's served directly - If it's
.jsx, the closest layout is used by going back through the directories
Using a custom key
If your views path depends on __dirname (e.g., in a library that needs to locate its own view files after bundling), you must provide a key option:
app.views(__dirname + "/views", { key: "my-library-views" });Without a key, the path is normalized relative to process.cwd(), which may not work correctly when bundled.
Important
The html, jsx, and views methods are not dynamic, so after listen they will no longer have an effect. This is because in production there is no build for security reasons.
If you want to create or remove routes after listen, you must do it as you would with Express.
Sub-apps
You can mount a lexpress instance inside another with app.use(), just like Express. The sub-app can be mounted on the same route (app.use(admin)) or with a base-url (app.use("/admin", admin)):
const lexpress = require("lex-press");
const app = lexpress();
const admin = lexpress();
app.views("./views");
admin.views("./views-admin");
// admin lives under /admin
app.use("/admin", admin);In the development server, the HTML of a sub-app with base-url adapts automatically: its assets (/__assets/...) and the hot-reload script are served with the base prefix (/admin/__assets/...).
In production, mount each app on a single base (see the Production section).
Development
The default mode of the framework is development, so you can run the development server simply with:
node index.jsor with nodemon for hot-reload
nodemon --watch ./ --ext js,jsx,ts,tsx,json,html,css --exec "node index.js"Build
To compile the server, all you need to do is add the --build flag to the main process:
node index.js --buildThis will compile the production server into .lex-press-app/server.js.
If your project uses non-bundleable libraries (whether because they use binaries or any other reason), we can pass a list of external dependencies:
node index.js --build --external external-module1 external-module2You can also use a series of esbuild flags if you need to customize the export:
- build
- format
- platform
- target
- bundle
- minify
- sourcemap
- treeShaking
- external
- tsconfig
Important about __dirname
When bundling, __dirname in your code will resolve to the .lex-press-app/ directory, not your original project directory. However, __dirname is a valid option for resolving paths to views or public if you provide a key option to those methods:
app.views(__dirname + "/views", { key: "my-views" });
app.public(__dirname + "/public", { key: "my-public" });For other routes and file resolutions, use process.cwd() or an environment variable instead.
Production
Once bundled, you can run your server from the .lex-press-app/server.js file:
node .lex-press-app/server.jsThe .lex-press-app folder also contains compiled HTML files, assets, and copies of public folders you've added to the project with app.public(). Therefore, this folder alone can contain everything necessary for a deployment that only uses app.public() and app.views().
The server file should only be executed from outside the folder.
Sub-apps and base-url
Each app's compiled HTML is escaped only once, with the base of the first request it receives (the tree is static). If you mount the same instance on two bases, only the first one will serve HTML with correct asset paths: mount each app on a single base.
About Express middleware
Lexpress exposes all the properties of the Express function, so you simply need to call app.use(lexpress.json()) to access this functionality, just as you would in Express.
