Deployment & Production Builds
Avenx-JS applications can be built into static JavaScript and CSS assets and served from a static hosting service or production web server. This guide explains how to create a production build, configure build-time settings and environment variables, deploy the generated files, and configure hosting for production.
How a build is produced
Section titled “How a build is produced”avenx build runs two halves with a clean boundary between them.
The compiler owns everything Avenx-specific: templates, <state> and
<action> declarations, expression handling, scoped CSS, the Atlas, and the
shape of a generated component class. It turns each .component.js, .page.js,
.bridge.js and .guard.js into an ordinary ES module.
The bundler owns everything module-specific: resolving specifiers, walking the dependency graph, eliminating what nothing reaches, and emitting the final script and its source map. It knows nothing about components or bridges.
.component.js / .page.js → compiler (templates, declarations, scoped CSS, Atlas) → ES modules → bundler (resolution, graph, tree shaking, emission) → dist/bundle.jsThe Avenx runtime is a normal dependency in that graph, resolved through
avenx-core/runtime like any other import. It is not a prebuilt file that gets
prepended to your application.
Imports
Section titled “Imports”Your import statements are resolved, not rewritten. That means:
- npm packages work, from components, pages, bridges and guards alike. Both
ES modules and CommonJS packages are supported; a package’s
exports,browser,moduleandmainfields are honoured, preferring its browser and ES builds. - Local modules work — by path, by extension-less path, or through a
directory
index.js. - An import that cannot be resolved fails the build, with the specifier and the file that asked for it. Nothing is ever dropped silently.
An imported value is also in scope for your templates and <action> bodies,
below your own <state>, computed values, actions and bridges — so nothing you
import can shadow something you declared.
Two kinds of import are rejected on purpose, because a browser cannot honour
them: Node built-ins such as fs and path, and asset imports such as
import './theme.css'. Component styles belong in a matching .component.css,
application-wide styles in a <@global> block, and a genuinely external
stylesheet in a <link> tag in index.html.
Tree shaking
Section titled “Tree shaking”A module reaches the bundle when something imports it. A plain import always
executes its target, as the language requires; a re-export is followed only for
the names something asked for, which is what lets avenx-core/runtime — a
barrel of re-exports — be shaken at all. A re-export target that runs code at
its own top level, or whose package declares sideEffects honestly, is kept
regardless.
In practice that means a production build does not carry the trace recorder,
because nothing in it can start a recording. A development build does, because
avenx serve --trace installs it.
Set "treeShake": false in avenx.config.json to keep everything the graph
reaches. It cannot resurrect a file nothing imports — such a file was never in
the graph.
The build reports what it did:
Bundled 66 modules · 5 shaken outDynamic imports, and code splitting
Section titled “Dynamic imports, and code splitting”import('./thing.js') works. The module joins the graph and is bundled, and the
promise resolves with its namespace:
const { renderChart } = await import('./charts/renderer.js');What it does not do yet is produce a separate file. Avenx emits one chunk, so a dynamically imported module is bundled eagerly and the promise resolves immediately. That is the correct observable behaviour for an unsplit build — the semantics are right, and what is missing is the chunk boundary — but it means a dynamic import is a code-organisation tool today, not a payload-reduction one.
The specifier must be a literal. import(name) fails the build, because no
build-time analysis can say what it names and the emitted bundle is a classic
script with no module loader to resolve it at run time.
Why splitting is not implemented. Emitting chunks is not the hard part; the
pieces around it are. dist/bundle.js is loaded with a plain <script src>, so
a second chunk needs a loader and a decision about whether the bundle becomes a
module script. Route-level laziness additionally needs initRouter to accept a
loader rather than a page name, and the router’s mount path to become
asynchronous. Those are application-facing changes, and the bundler now has the
graph they would build on: dynamic imports are already distinct edges, and
lib/bundler/emit.js already takes the set of modules to include as a
parameter.
Build Modes
Section titled “Build Modes”Avenx builds in one of two modes.
| Development | Production | |
|---|---|---|
| Command | npx avenx build --dev |
npx avenx build |
| Also used by | avenx serve, avenx watch |
— |
| Minified | No — comments and indentation kept | Yes |
| Trace recorder | Included, for avenx serve --trace |
Shaken out |
| CSS source map | Inline, for the browser devtools | Emitted as bundle.css.map and linked |
| Hello World bundle | ~470 KB | ~389 KB (~87 KB gzipped) |
Production is the default. npx avenx build is what a deploy script runs, so it produces optimised output without a flag. avenx serve and avenx watch build in development mode, because a readable stack trace matters more than size while you are working. Either default can be overridden with --dev or --prod, or pinned in avenx.config.json:
{ "mode": "production"}NODE_ENV=development also selects development mode. The active mode is printed in the build header:
--- Avenx-JS Compiler (production) ---What the production build does
Section titled “What the production build does”- Minifies. Comments and indentation are removed. Avenx’s minifier does not rename identifiers or join statements: doing that safely needs a full ECMAScript parser, and a minifier that guesses produces a bundle that is smaller and wrong. It buys one property with the bytes it leaves behind — line count is preserved exactly, so a source map stays valid across minification and a production stack trace names a line you wrote.
- Shakes out what nothing reaches. See above.
- Excludes development infrastructure. The testing utilities
(
avenx-core/testing) and the lint and build helpers (avenx-core/tooling) are separate entry points and can never be part of an application bundle. Neither can Node built-ins: an import of one fails the build. - Keeps the global surface small. See below.
Squeezing it further
Section titled “Squeezing it further”dist/bundle.js is a plain classic script with a valid source map beside it, so
any minifier can post-process it. Identifier mangling is where the remaining
size is: on the scaffolded project, running the production bundle through a
mangling minifier takes it from 389 KB / 87 KB gzipped to 230 KB / 66 KB
gzipped. Avenx does not do this for you, because it will not ship a
transformation it cannot prove safe.
What the bundle puts on globalThis
Section titled “What the bundle puts on globalThis”Generated modules import what they use, so the global object is a compatibility surface rather than a mechanism. The bundle installs:
Avenx— a namespace carrying the seven names below.- Seven named globals —
AvenxComponent,AvenxPage,defineBridgeName,AvenxApp,AvenxGuard,AvenxRouterandbridge.
Anything else the runtime exports is reached by importing it:
import { logger, LruCache } from 'avenx-core/runtime';That import is resolved by the bundler and only what you name is bundled. Publishing the whole export surface on the global object would pin every runtime module into every application.
If your page loads other scripts, these eight names are the only ones Avenx claims.
Build Failures and Exit Codes
Section titled “Build Failures and Exit Codes”avenx build exits 0 only when the application compiled and the output was written. Any fatal error exits non-zero, so a chained deployment step never runs after a failed build:
npx avenx build && ./deploy.shIf compilation fails, deploy.sh does not execute and CI marks the job as failed.
What fails the build
Section titled “What fails the build”| Condition | Code |
|---|---|
| The source directory is missing | AVX_C02 |
| Two components compile to the same class name | AVX_C03 |
A bridge cannot be resolved, is duplicated, is circular, or is not a bridge() module |
AVX_C07–AVX_C12 |
A warning escalated to "error" in avenx.config.json |
that warning’s code |
A prebuild or postbuild hook exits non-zero |
AVX_C14 |
| The output directory cannot be written | AVX_C01 |
| Any unexpected error during compilation | — |
Warnings that are not escalated stay warnings: they are printed and the build still exits 0. To make one fatal, set its severity in avenx.config.json:
{ "warnings": { "AVX_W03": "error" }}What a failure looks like
Section titled “What a failure looks like”--- Avenx-JS Compiler (production) ---
✖ Build failed
[AVX_C03] Duplicate component name(s) detected. These files compile to the same class name: "Card": - src/components/card/card.component.js - src/components/widgets/card.component.jsFix by renaming or moving one of the files.
The command exits with a non-zero status.A diagnosed error prints its message alone; an unexpected error prints its stack, because that is the only useful thing it carries.
Output is written atomically
Section titled “Output is written atomically”Artifacts are compiled in full, written to a staging directory inside distDir, and renamed into place only once the entire build succeeds. A build that fails at any point — including after the artifacts are staged — promotes nothing, so distDir never holds a mix of a new script and a stale stylesheet.
A failed build does not delete the previous output. The exit code is what stops the deployment; removing a working bundle would break anything still serving it. The previous artifacts stay exactly as they were, byte for byte.
serve and watch are the exception
Section titled “serve and watch are the exception”avenx serve and avenx watch keep running when a rebuild fails. The error is printed, the browser is not reloaded so it keeps showing the last good build, and watching continues — the next save usually fixes it. Neither command gates a deployment, and neither sets a failing exit code for a rebuild error.
Anatomy of a Production Build
Section titled “Anatomy of a Production Build”By default, the compiler uses the following configuration:
srcDir:"src"distDir:"dist"outputName:"bundle"
This produces the following generated structure:
dist/├── bundle.js├── bundle.css└── bundle.css.mapbundle.js: The generated JavaScript bundle.bundle.css: The generated CSS bundle.bundle.css.map: A CSS source-map artifact useful for debugging. It is not required for the application to function.
The index.html file is not copied into dist/ by npx avenx build. It remains in the project root and must be deployed alongside the generated dist/ assets.
A minimal index.html for the default output looks like this:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>My Avenx App</title> <link rel="stylesheet" href="dist/bundle.css"></head><body> <div id="app"></div> <script src="dist/bundle.js"></script></body></html>Customizing outputName
Section titled “Customizing outputName”Changing the outputName in your avenx.config.json:
{ "outputName": "app"}Produces the following assets instead:
dist/app.jsdist/app.cssdist/app.css.mapAvenx-JS does not automatically update your index.html file when the outputName changes. You must manually update index.html to reference the matching filenames.
Build-Time Configuration
Section titled “Build-Time Configuration”srcDir and distDir
Section titled “srcDir and distDir”The srcDir option controls the application source directory, while distDir controls the build output directory. Both are configurable in avenx.config.json. By default, they are set to src and dist.
{ "srcDir": "src", "distDir": "dist"}Environment Variable Interpolation
Section titled “Environment Variable Interpolation”Avenx-JS supports ${ENV_VAR} interpolation within avenx.config.json.
{ "distDir": "${BUILD_DIR}"}The environment variable is resolved from the process environment before the configuration validation runs.
Build Lifecycle Hooks
Section titled “Build Lifecycle Hooks”You can define synchronous shell commands that execute before and after the compiler runs. The hooks.prebuild command runs before the compiler build, and hooks.postbuild runs after the compiler build returns successfully. Both commands execute from the project base directory.
{ "hooks": { "prebuild": "npm run prepare", "postbuild": "npm run verify" }}The execution lifecycle follows this exact sequence:
prebuild → compiler build → postbuild
Environment Variables & Security
Section titled “Environment Variables & Security”Avenx-JS loads project environment configuration at build time. Variables that use the AVX_PUBLIC_ mechanism are intended specifically for client-side exposure.
.env Loading
Section titled “.env Loading”Avenx-JS loads the .env file from the project root directory. The file is parsed and its values are loaded into the Node.js process.env object.
- Existing
process.envvariables are not overwritten by values from the.envfile. - Loading the
.envfile does not mean every variable is exposed to the browser.
AVX_PUBLIC_API_URL=https://api.example.comAPI_INTERNAL_URL=https://internal.example.comThe AVX_PUBLIC_ Prefix Rule
Section titled “The AVX_PUBLIC_ Prefix Rule”During the build process, Avenx replaces references to process.env.AVX_PUBLIC_* with their corresponding values from process.env. These values are stringified and inlined into the generated client-side JavaScript bundle.
const apiUrl = process.env.AVX_PUBLIC_API_URL;Never put secrets in an AVX_PUBLIC_ variable. Values referenced through process.env.AVX_PUBLIC_* are replaced at build time and can become part of the generated client-side JavaScript bundle, so they should be treated as public.
Never use this prefix for passwords, database credentials, private API keys, private access tokens, authentication secrets, or other confidential values.
Verifying Environment Configuration
Section titled “Verifying Environment Configuration”avenx envTo see exactly what the compiler is exposing, run the avenx env command. This tool displays the AVX_PUBLIC_* variables as public and masks all other non-public variables loaded from your .env file.
Static Hosting
Section titled “Static Hosting”Avenx-JS production builds produce entirely static assets. They require no server-side runtime and can be served by any static hosting provider or traditional web server.
Because Avenx-JS uses the BrowserNavigationDelegate to read window.location.hash, all application routes are stored in the URL hash fragment. For example, if you serve your application from a sub-path, the URL will look like /my-app/#/dashboard.
When a user visits /my-app/#/dashboard, the server only receives a request for /my-app/, not /my-app/#/dashboard, because the fragment is handled entirely by the browser. Therefore, the server only needs to serve the application entry point and your static assets. You do not need to configure SPA rewrite rules, fallback rules (such as Nginx try_files), Apache rewrites, or _redirects files for Avenx-JS routing.
GitHub Pages
Section titled “GitHub Pages”To deploy to GitHub Pages, build your application using:
npm cinpx avenx buildWhen deploying to a project repository, GitHub Pages will automatically serve your site from a sub-path, such as https://username.github.io/my-app/. Because Avenx-JS uses hash routing, it naturally handles sub-paths. You do not need to configure a base, publicPath, or deployment sub-path setting in your avenx.config.json—no such option exists. (Note: The internal baseDir configuration is for local filesystem resolution, not deployment URLs).
Simply ensure that both your root index.html file and your generated dist/ directory are included in the GitHub Pages artifact.
GitHub Actions
Section titled “GitHub Actions”GitHub Pages can also be deployed automatically with GitHub Actions. The complete workflow is documented in the “CI/CD Integration” section.
Netlify
Section titled “Netlify”To deploy to Netlify, set your build command to npx avenx build. Because your index.html remains in the project root while the compiled assets are in dist/, you must configure your publish directory to a location that preserves this structure:
index.htmldist/├── bundle.js├── bundle.css└── bundle.css.mapYou do not need to add a _redirects file for application routing.
Nginx / Apache
Section titled “Nginx / Apache”To host your application on Nginx, Apache, or another static web server, copy your index.html file and the dist/ directory to your static web root (e.g., /var/www/html/my-app/).
You do not need to configure any catch-all rewrite directives. The server’s default configuration for serving static files is completely sufficient.
Caching & Security Headers
Section titled “Caching & Security Headers”Avenx-JS does not automatically configure production cache headers or Content Security Policy headers. These must be configured by the hosting provider or web server.
Caching Generated Assets
Section titled “Caching Generated Assets”Avenx-JS does not generate content-hashed filenames. The default generated filenames are:
bundle.jsbundle.cssbundle.css.map
dist/├── bundle.js├── bundle.css└── bundle.css.mapWhile the outputName configuration option can change the filename, the filename itself does not automatically change based on file contents. Therefore, users should avoid assuming that a static filename such as bundle.js provides automatic cache busting.
Production hosting environments should use appropriate cache validation and revalidation behavior for these static assets. By utilizing mechanisms such as ETags or Last-Modified headers, you can ensure that clients do not unnecessarily keep stale versions of the application after a new deployment.
CSS Source Maps
Section titled “CSS Source Maps”The bundle.css.map file is a debugging and source-map artifact. It is not required for production runtime execution. You can include it in your production deployments when source-map debugging is desired, or you may safely omit it if production source maps are not wanted. Removing it will not break the application.
Content Security Policy (CSP)
Section titled “Content Security Policy (CSP)”A production build never needs 'unsafe-eval'.
Content-Security-Policy: script-src 'self';Everything executable in an Avenx application — template interpolations,
computed values, directive bindings, list keys, inline event handlers,
<action> bodies and <resource> handlers — is compiled to ordinary
JavaScript functions at build time and linked into your bundle. The
browser’s own engine compiles them when it compiles the rest of your code.
Nothing in a production bundle calls eval, constructs a function from a
string, or uses a with statement.
That is checked rather than claimed: the test suite builds a real application
and asserts the emitted bundle contains no new Function, no with, and none
of the parser or evaluator that used to back them.
What changed
Section titled “What changed”Earlier versions shipped expressions and action bodies to the browser as
source text and interpreted them there. Expressions were walked as a tree,
which needed no eval — but an action body using real statement syntax (if,
for, try, return, a declaration) was compiled with new Function the
first time it ran. In practice that meant most non-trivial applications needed
'unsafe-eval' after all.
Nothing about the security checks was lost in the move. A member read still
passes through one function with the key already resolved, so
x['const'+'ructor'] and x.constructor are the same check; the Function
constructor and built-in prototypes are still unreachable. Two checks moved
earlier: naming a restricted global (window, fetch, localStorage) in a
template expression or inline handler, or writing a forbidden key (__proto__,
constructor, prototype), now fails the build with a file and a line rather
than throwing when that branch first runs. <action> and <resource> bodies are
ordinary JavaScript and may use browser APIs; only eval and Function are
refused there (see Template expressions).
Development builds
Section titled “Development builds”avenx build --dev and avenx serve do include the expression interpreter, so
that a template you are still editing keeps rendering when it contains something
the compiler could not compile. A development build therefore does contain
new Function, and is not intended to be deployed.
If the build reports AVX_W48 or AVX_C27
Section titled “If the build reports AVX_W48 or AVX_C27”AVX_W48 (development) and AVX_C27 (production) list every expression or body
the compiler could not turn into a function, with its component, file, line and
reason. A production bundle has no interpreter, so a production build fails
with AVX_C27 rather than shipping an expression that would throw when it is
first evaluated; nothing is written and the previous dist/ is left intact. A
development build reports AVX_W48 and still builds, so the rest of a
component you are editing keeps rendering.
Rewrite each one in the supported expression language, or move the logic into an
<action>, whose body is ordinary JavaScript.
Before 2026-09, a production build only warned (
AVX_W48) and reported success, and the expressions failed in the browser. A project whose production build now stops withAVX_C27was already shipping those failures.
Hosting Configuration
Section titled “Hosting Configuration”Avenx-JS does not automatically configure production cache headers or Content Security Policy headers. These headers must be configured by the production hosting platform, CDN, reverse proxy, or web server.
CI/CD Integration
Section titled “CI/CD Integration”Avenx-JS applications can be built and deployed automatically through CI/CD pipelines.
The basic production pipeline consists of the following steps:
- Install dependencies with
npm ci. - Build the Avenx application with
npx avenx build. - Prepare a deployment artifact containing the root
index.htmland the generateddist/directory. - Deploy that artifact to the selected static hosting platform.
GitHub Actions
Section titled “GitHub Actions”GitHub Actions can automate the production build and deployment of an Avenx application.
The following workflow example demonstrates a deployment to GitHub Pages. Because Avenx-JS keeps index.html outside of the dist/ directory, the workflow creates a temporary _site directory to prepare the final artifact.
name: Deploy Avenx App
on: push: branches: - main
permissions: contents: read pages: write id-token: write
jobs: deploy: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v4
- uses: actions/setup-node@v4 with: node-version: 22 cache: npm
- name: Install dependencies run: npm ci
- name: Build Avenx application run: npx avenx build
- name: Configure GitHub Pages uses: actions/configure-pages@v5
- name: Prepare Pages artifact run: | mkdir -p _site cp index.html _site/ cp -r dist _site/
- name: Upload Pages artifact uses: actions/upload-pages-artifact@v3 with: path: _site
- name: Deploy to GitHub Pages uses: actions/deploy-pages@v4The resulting artifact structure uploaded to GitHub Pages will be:
index.htmldist/├── bundle.js├── bundle.css└── bundle.css.mapNote that the bundle.css.map file is optional and is not required for deployment, as explained in the Caching & Security Headers section.
CI Build Without Deployment
Section titled “CI Build Without Deployment”The same CI process can be used solely for validation and build verification without deploying to a hosting provider.
npm cinpx avenx buildThis allows continuous integration systems to verify that the application dependencies can be installed and that the project successfully compiles without errors.
Deployment Artifact
Section titled “Deployment Artifact”When deploying to any environment, the deployment artifact must preserve the relationship between index.html and dist/.
Because index.html references the generated assets at dist/bundle.js and dist/bundle.css, the deployed artifact must maintain that exact directory structure. You should not move or modify index.html in relation to the dist/ directory unless specifically required by a specialized hosting platform.
Why Not avenx serve?
Section titled “Why Not avenx serve?”The avenx serve command is designed exclusively for local development and should not be used as the production deployment mechanism.
Development Features
Section titled “Development Features”avenx serve provides development-oriented functionality such as live reload.
It provides an active Server-Sent Events (SSE) endpoint used by the development server’s live-reload functionality:
/__avenx_live_reload__It also exposes a developer-oriented inspection dashboard that is part of the development server:
/__avenx-inspectProduction Deployment
Section titled “Production Deployment”Production deployments should use:
npx avenx buildThe build generates the static application assets, which can then be served by a production static host, CDN, reverse proxy, or web server.
The avenx serve command does not provide production-oriented features such as gzip/Brotli compression or TLS.
Development vs Production
Section titled “Development vs Production”| Use Case | Command | Purpose |
|---|---|---|
| Local development | avenx serve |
Development server with live reload and inspection tools |
| Production build | npx avenx build |
Generates static assets for production hosting |