@broxium/compiler
v1.7.0
Published
Brodox component compiler — TSX to ESM server + client bundles
Readme
@broxium/compiler
The Brodox component compiler. Takes a developer's component source files (TSX/JSX) and compiles them into two versioned ESM bundles — one for server-side rendering and one for browser hydration.
This is an internal package used by the Brodox platform. Developers building components do not use this directly; it is called automatically when a component version is approved in the Brodox dashboard.
Installation
npm install @broxium/compilerNode.js 20+ is required.
What it does
When a component version is approved, BrodoxCompiler.compile() is called with the component's source files. It produces two output files in LIVE_COMPONENTS_PATH:
| Output file | Purpose | Format |
|---|---|---|
| {slug}-v{version}.server.esm.js | Loaded by the web engine via Node.js import() for SSR | Pure ESM, Node 20 target, readable (not minified) |
| {slug}-v{version}.client.esm.js | Served to the browser for island hydration | Pure ESM, ES2020 / Chrome 90 / Firefox 88 / Safari 14, minified |
Both bundles treat react, react-dom, react/jsx-runtime, and @broxium/runtime as externals — they are never bundled in. The web engine's import map and the browser's @broxium/runtime static file provide these at runtime.
Usage
import { BrodoxCompiler } from '@broxium/compiler'
const compiler = new BrodoxCompiler()
const result = await compiler.compile({
componentId: 42,
slug: 'hero-banner',
version: '1.0.4',
files: [
{
path: 'App.tsx',
content: `
import { BrodoxImage } from '@broxium/runtime'
export default function HeroBanner({ title, image }) {
return (
<section>
<h1>{title}</h1>
<BrodoxImage src={image} alt={title} width={1280} />
</section>
)
}
`,
},
],
outputDir: '/brodox-developer-drive/web-components/live-bundles',
})
console.log(result.serverJsName) // "hero-banner-v1.0.4.server.esm.js"
console.log(result.clientJsName) // "hero-banner-v1.0.4.client.esm.js"
console.log(result.serverJsPath) // "/brodox-developer-drive/.../hero-banner-v1.0.4.server.esm.js"
console.log(result.compiledAt) // Date objectAPI
new BrodoxCompiler()
Creates a new compiler instance. The instance is stateless and safe to reuse across multiple calls — the Brodox platform creates a single instance per process.
compiler.compile(input): Promise<CompileOutput>
Compiles a component's source files into server and client ESM bundles.
CompileInput
interface CompileInput {
componentId: number // DB row ID — not used in output naming, reserved for future use
slug: string // Component slug, e.g. "hero-banner"
version: string // Semver string, e.g. "1.0.4"
files: Array<{
path: string // Relative path within the component, e.g. "App.tsx" or "utils/format.ts"
content: string // Full source text
}>
outputDir: string // Absolute path where output files are written
}CompileOutput
interface CompileOutput {
serverJsPath: string // Absolute path to the server bundle
clientJsPath: string // Absolute path to the client bundle
serverJsName: string // Filename only: "{slug}-v{version}.server.esm.js"
clientJsName: string // Filename only: "{slug}-v{version}.client.esm.js"
compiledAt: Date // Timestamp of compilation
}Entry point resolution
The compiler looks for the entry file by checking the files array in this priority order:
App.tsx → App.jsx → App.ts → App.js →
index.tsx → index.jsx → index.ts → index.jsIf none of these are found, the compiler throws:
Error: No entry file found in component files for {slug}Temporary directory
Source files are written to a temporary directory under os.tmpdir() before compilation. The directory is automatically cleaned up after compilation completes (or fails with an error that is re-thrown).
config.json format
Every component must include a config.json file alongside App.tsx. This file defines the props exposed in the website builder's property inspector panel.
The format is a flat object — prop name as key, field definition as value. Do not use a name/slug/props wrapper.
{
"title": {
"label": "Title",
"type": "text",
"default": "Hello World"
},
"count": {
"label": "Item Count",
"type": "number",
"default": 3
},
"theme": {
"label": "Theme",
"type": "select",
"options": ["light", "dark"],
"default": "light"
}
}Supported field types
| type | Builder input | Extra keys |
|---|---|---|
| text | Text input | — |
| textarea | Multiline textarea | — |
| url | URL input (validated) | — |
| number | Number input | — |
| select | Dropdown | options: string[] |
| color | Color picker + hex | — |
| boolean | Checkbox | — |
| range | Slider | min, max |
| brodox-* | Custom Brodox field | varies |
Optional field keys
| Key | Type | Description |
|---|---|---|
| label | string | Display name in the inspector panel |
| type | string | Input type (required) |
| default | any | Initial value when component is first dropped |
| render | "client" | "server" | Badge shown in inspector (cosmetic only) |
Common crash: If
config.jsonis not a flat object (e.g. has a top-levelnameorpropsarray key), the builder will crash withCannot read properties of undefined (reading 'startsWith')when the component is dragged onto the canvas.
'use client' and 'use server' handling
The two esbuild builds use custom plugins to handle React-style directives:
Server bundle — clientStubPlugin
Any file whose first non-whitespace characters are 'use client' or "use client" is replaced with a stub:
import React from 'react'
export default function ClientStub() { return null }
export function getServerData() { return {} }This means client-only sub-components silently render nothing on the server, which is correct — their interactive version will be rendered entirely by the browser.
Client bundle — serverStubPlugin
Any file whose first non-whitespace characters are 'use server' or "use server" is replaced with:
export default function ServerStub() { return null }Server-only code never runs in the browser.
Entry file directive
The directive on the entry file (App.tsx) determines the renderMode stored in the Page Manifest:
| Entry file starts with | renderMode | Server renders | Client hydrates | Use when |
|---|---|---|---|---|
| 'use client' | client | No | Yes (full) | useState, useEffect, event handlers, browser APIs |
| 'use server' | server | Yes (full) | No | Display-only, no interactivity needed |
| (nothing) | both | Yes | Yes (islands only) | Static shell with <Client> islands for interactive parts |
Note: These directives are not the same as Next.js.
'use server'here means "exclude from client bundle" — it does not create a server action.
Common mistake — missing 'use client'
Using useState, useEffect, or any hook at the top level of the entry file without 'use client' causes the web engine to crash during SSR:
Cannot read properties of null (reading 'useState')Fix: add 'use client' as the very first line of App.tsx.
'use client';
import { useState } from 'react';
export default function App() {
const [count, setCount] = useState(0);
// ...
}Sub-file directives
You can split a multi-file component by marking individual files:
App.tsx ← no directive (both: SSR shell + client hydration)
├── Counter.tsx ← 'use client' (stubbed on server, runs in browser)
└── DataTable.tsx← 'use server' (stubbed in browser, runs on server)External dependencies
The following packages are always treated as external in both bundles:
| Package | Why external |
|---|---|
| react | Provided by the web engine's import map / browser global |
| react-dom | Same |
| react/jsx-runtime | Same |
| react/jsx-dev-runtime | Same |
| @broxium/runtime | Provided by /static/broxium-runtime.js on the web engine |
Any other import in the component source will be bundled in. This means third-party libraries like date-fns, zod, or clsx are embedded in the output files. Dependencies must be whitelisted in the Brodox platform's allowed-package list before they can be used in a component.
Output file naming
Output files follow a deterministic naming convention:
{slug}-v{version}.server.esm.js
{slug}-v{version}.client.esm.jsExamples:
| Slug | Version | Server file | Client file |
|---|---|---|---|
| hero-banner | 1.0.4 | hero-banner-v1.0.4.server.esm.js | hero-banner-v1.0.4.client.esm.js |
| product-grid | 2.1.0 | product-grid-v2.1.0.server.esm.js | product-grid-v2.1.0.client.esm.js |
Because the version is embedded in the filename, approving a new version always produces new files. The old files remain on disk for rollback purposes and are never overwritten.
Integration with BundleService
In the Brodox platform, BrodoxCompiler is called from BundleService in sub-app-brodox-site-engine-app. BundleService reads all source files from the component's file_path directory on disk and passes them to the compiler:
// BundleService.ts (simplified)
import { BrodoxCompiler } from '@broxium/compiler'
const compiler = new BrodoxCompiler()
async function compileBothBundles(folderPath, slug, version) {
const files = await readComponentFiles(folderPath) // reads all .tsx/.jsx/.ts/.js/.css/.json
const result = await compiler.compile({
componentId: 0,
slug,
version,
files,
outputDir: process.env.LIVE_COMPONENTS_PATH,
})
return result
}Error handling
The compiler throws on any of the following conditions:
| Condition | Error message |
|---|---|
| No entry file found | No entry file found in component files for {slug} |
| esbuild server build fails | esbuild error message (TypeScript error, import not found, etc.) |
| esbuild client build fails | esbuild error message |
Errors from the server build are thrown before the client build is attempted. The temporary directory is cleaned up even when an error is thrown.
When BundleService catches a compilation error, it blocks the component from being approved and returns the error message to the reviewer.
runtimeServerStubPlugin — @broxium/runtime server stubs
During server bundle compilation, all @broxium/runtime imports are replaced
by inline server-safe stubs so the server bundle has zero external dependencies.
Key stub behaviours:
| Export | Server stub renders |
|---|---|
| BrodoxLink | <a data-brodox-link href={href}> — the data-brodox-link attribute is required for the shell's click interceptor |
| BrodoxImage | <img src="/api/image?..."> with srcset, or raw src if direct={true} |
| Client | Empty placeholder div + sibling <script type="application/json"> with props |
| Server | Transparent passthrough (Fragment) |
| useRouter, useParams | Static no-ops (return empty objects) |
| BrodoxHead, BrodoxFont | null |
Important: If you compile a component and the rendered <a> tags do not
have data-brodox-link, the shell will not intercept clicks on those links and
the browser will do a full page reload. This was fixed in v1.3.2 — ensure all
projects use @broxium/compiler@^1.3.2 or later. Existing pre-1.3.2 server
bundles must be patched manually or recompiled.
Package info
| | |
|---|---|
| Package | @broxium/compiler |
| Version | 1.3.3 |
| Formats | ESM (dist/index.mjs), CJS (dist/index.js) |
| Types | dist/index.d.ts |
| Runtime dependency | esbuild ^0.25 |
| Node.js requirement | 20+ |
| Side effects | Writes files to outputDir, creates/removes temp directory |
Changelog
| Version | Change |
|---|---|
| 1.3.3 | BrodoxImage stub: added direct prop support |
| 1.3.2 | BrodoxLink stub: added data-brodox-link attribute |
| 1.3.1 | Initial public release |
