MishkaCmsCore. Runtime. Compilers. JavaScriptCompiler
(Mishka CMS Core v0.0.1)
Copy Markdown
View Source
Production-ready JavaScript compiler for runtime JS generation.
This module provides comprehensive JavaScript compilation functionality for MishkaCMS, combining application JS files, vendor dependencies, and database-stored JavaScript hooks into optimized, site-specific bundles.
Features
- Runtime JS Hook Compilation: Dynamically compiles JavaScript hooks from database
- Site-specific Bundling: Creates optimized JS bundles per site configuration
- Phoenix Colocated Integration: Supports Phoenix colocated JavaScript hooks
- ESBuild Integration: Uses ESBuild for fast, modern JavaScript bundling
- Asset Compression: Automatic Brotli and Gzip compression for optimal delivery
- Asset State Management: Caches compiled JS in memory for fast serving
- Automatic Cleanup: Manages temporary files and build artifacts
- Error Handling: Comprehensive error reporting and recovery
JavaScript Architecture
The compiler creates a hierarchical JavaScript structure:
Entry File (app.js + runtime hooks)
├── Base Application JavaScript (app.js)
├── Phoenix Colocated Hooks (phoenix-colocated/mishka_cms.js)
└── Site-specific Runtime Components
├── Global JavaScript Hooks (database)
└── Site-specific JavaScript Hooks (database)Compilation Process
- Prepare Environment: Creates temporary directories and asset structure
- Generate Runtime Hooks: Fetches JS hooks from database and creates component files
- Ensure Colocated Hooks: Sets up Phoenix colocated JavaScript integration
- Create Entry File: Combines app.js with runtime components
- Execute ESBuild: Bundles and minifies JavaScript using ESBuild
- Compress Assets: Creates Brotli and Gzip versions for optimal delivery
- Store Results: Caches compiled JS in AssetsState for fast serving
- Cleanup: Removes temporary files and build artifacts
Usage
# Compile JavaScript for a specific site
{:ok, js} = JavaScriptCompiler.compile("my_site")
# Compile and store in cache
{:ok, js} = JavaScriptCompiler.compile_and_store("my_site")
# Retrieve cached JavaScript
js = JavaScriptCompiler.get_compiled_js("my_site")
# Check if JavaScript exists
exists = JavaScriptCompiler.js_exists?("my_site")
# Refresh JavaScript (recompile and update cache)
{:ok, js} = JavaScriptCompiler.refresh_js("my_site")ESBuild Configuration
The compiler uses ESBuild with the following configuration:
- Target: ES2022 for modern browser support
- Format: IIFE (Immediately Invoked Function Expression) for browser compatibility
- Platform: Browser-specific optimizations
- Minification: Enabled for production builds
- External Resources: Excludes fonts and images from bundling
- Alias Support: Uses @ alias for cleaner imports
Error Handling
Compilation failures are handled gracefully with:
- A hook that does not build is left out and the rest of the bundle is built without it; the
reason is read with
left_out/1and sent asruntime:compile:js_hook_skipped - Detailed error messages including ESBuild output
- Automatic cleanup of temporary files
- Logging of compilation status and errors
- Fallback behavior for missing JavaScript
Lazy hooks
A hook marked lazy is not built into the bundle. It is built on its own, in the same compile, as
an ES module whose name is the hash of its content, and stored beside the bundle under
lazy_type/1. The bundle registers a stub under the hook's browser name instead; the first element
that mounts it imports the module once, and from then on the hook's own mounted, beforeUpdate,
updated, destroyed, disconnected and reconnected run on that element. A page that never
mounts the hook never downloads it. Each module passes through runtime:compile:lazy_js, with
%{site: site_name, hook: hook_name} as its meta, before it is hashed — as the bundle passes
through runtime:compile:js.
The module is read beside the bundle that asked for it — /assets/js/<site>/<hash>.js, with the
bundle's query string — so the bundle has to be loaded from its URL, not inlined. It is served
by MishkaCmsCoreWeb.AssetsController to whoever may read the bundle, with the same headers, and
replaced with it: the modules a bundle names are the ones stored under its site. A same-origin
script, it needs nothing from the Content-Security-Policy that 'self' does not already give.
A lazy hook's default export is a hook object, or a hook class whose state is set in mounted:
its constructor does not run.
Integration with Phoenix
# In your layout template
<script>
<%= raw(MishkaCmsCore.Runtime.Compilers.JavaScriptCompiler.get_compiled_js(@site_name)) %>
</script>
# Or serve as static asset with compression
defmodule MyAppWeb.AssetController do
def javascript(conn, %{"site" => site_name}) do
js = JavaScriptCompiler.get_compiled_js(site_name)
conn
|> put_resp_content_type("application/javascript")
|> put_resp_header("cache-control", "public, max-age=31536000")
|> send_resp(200, js)
end
end
Summary
Functions
The name a runtime hook is registered under in the browser.
Compiles JavaScript from app.js, vendor files, and hooks for a specific site.
Compiles and stores the JS in AssetsState for a specific site.
Compiles + stores the shared base JS (app.js + global hooks only) under
__shared_base__. Built once and reused by every global-only site.
Deletes compiled JS for a site.
Drops the cached shared base JS (all formats), and the lazy hooks' modules it loads.
Ensures the configured ESBuild is installed and runnable, caching the answer in :persistent_term
so the check costs one --version run per node rather than one per compile.
Retrieves compiled JS from AssetsState for a specific site.
Cached shared base JS, compiling + caching it on first use.
Checks if compiled JS exists for a site.
The AssetsState type a lazy hook's module is stored under, beside its site's bundle, from the
hash its file is named by: lazy_js:<hash>, with brotli_lazy_js:<hash> and
gzip_lazy_js:<hash> for its compressed copies.
The bundles the JS hook hook_id was left out of when they were last built, each with the
reason, sorted by bundle: [{"localhost", ~s(Could not resolve "echarts" (globalChart.js:28))}].
Refreshes JS for a site (recompiles and updates).
Functions
The name a runtime hook is registered under in the browser.
Public because the builder's hook picker has to write exactly this string into phx-hook, and a
second copy of the rule would drift from the one that does the registering.
Compiles JavaScript from app.js, vendor files, and hooks for a specific site.
This is the main compilation function that orchestrates the entire JavaScript bundling process for a specific site. It handles temporary file management, runtime hook generation, and ESBuild execution.
Parameters
site_name- The name/identifier of the site to compile JavaScript for
Returns
{:ok, %{js: compiled_js, lazy: modules}}- On successful compilation: the bundle, and the module of each lazy hook it loads on first use, as%{hook: name, hash: hash, js: module}{:error, reason}- On failure, returns an error with descriptive message
Compilation Steps
- Creates temporary directories for build process
- Generates runtime hooks from database-stored JavaScript
- Ensures Phoenix colocated hooks are available
- Executes ESBuild bundling with optimizations
- Cleans up temporary files
Error Handling
Failures at any step will result in automatic cleanup of temporary files and return a descriptive error message. Common failure points:
- Missing app.js file
- ESBuild compilation errors
- Database connection issues when fetching hooks
- File system permission errors
Example
# Compile JavaScript for main site
case JavaScriptCompiler.compile("main") do
{:ok, %{js: js}} ->
IO.puts("Compiled JS size: #{byte_size(js)} bytes")
{:error, reason} ->
IO.puts("Compilation failed: #{reason}")
end
Compiles and stores the JS in AssetsState for a specific site.
This function combines compilation with caching, storing the compiled JavaScript along with compressed versions (Brotli and Gzip) in the AssetsState for fast serving.
Parameters
site_name- The name/identifier of the site to compile JavaScript for
Returns
{:ok, compiled_js}- On success, returns the compiled JavaScript string{:error, reason}- On failure, returns compilation error
Storage Details
Creates three entries in AssetsState:
"js"- Original compiled JavaScript"brotli_js"- Brotli-compressed version (best compression)"gzip_js"- Gzip-compressed version (wide compatibility)
Each entry includes:
- MD5 hash for cache validation
- Timestamp for cache invalidation
- Compressed content for optimal delivery
Example
# Compile and cache JavaScript
case JavaScriptCompiler.compile_and_store("main") do
{:ok, js} ->
Logger.info("JavaScript compiled and cached: #{byte_size(js)} bytes")
{:error, reason} ->
Logger.error("Failed to compile: #{reason}")
end
Deletes compiled JS for a site.
Removes all cached JavaScript assets for the specified site from AssetsState. This includes the original JS and all compressed versions.
Parameters
site_name- The site identifier to delete JavaScript for
Returns
:okon successful deletion- Error tuple if deletion fails
Usage
# Clean up old JavaScript
JavaScriptCompiler.delete_compiled_js("old_site")
# Force recompilation by deleting and recompiling
JavaScriptCompiler.delete_compiled_js("main")
JavaScriptCompiler.compile_and_store("main")
Ensures the configured ESBuild is installed and runnable, caching the answer in :persistent_term
so the check costs one --version run per node rather than one per compile.
It asks the binary, not the path. Esbuild.bin_path/0 BUILDS a string and returns it whether or
not a file sits at the end of it, so testing it told us only that esbuild has a name — a missing
binary then reached System.cmd and came back as ErlangError :enoent, an error naming neither
esbuild nor a path, underneath a log line saying the binary had been found.
The version is checked too, and not as a formality: bin_path/0 puts no version in the filename,
and Esbuild.configured_version/0 falls back to the library's own @latest_version when this
project's config is not loaded. So anything that installs esbuild from outside this config leaves
a differently-versioned binary at exactly the path we look at, and it stays there. That is how
0.25.0 came to sit where 0.25.4 was pinned.
Raises when esbuild is still unusable after installing, rather than caching a verification that is
not true. MishkaCmsCore.Runtime.Handlers.BootHandler expects that and boots without it.
Retrieves compiled JS from AssetsState for a specific site.
Fast retrieval of cached JavaScript without recompilation. Returns a fallback comment if JavaScript is not found for the specified site.
Parameters
site_name- The site identifier to retrieve JavaScript for
Returns
- Compiled JavaScript string if found in cache
- Fallback comment string if not found:
"// JS not found for site: {site_name}"
Usage
js = JavaScriptCompiler.get_compiled_js("main")
# In Phoenix template
<script><%= raw(js) %></script>
Checks if compiled JS exists for a site.
Quick boolean check to determine if JavaScript has been compiled and cached for the specified site.
Parameters
site_name- The site identifier to check
Returns
trueif compiled JavaScript exists in cachefalseif no JavaScript found for the site
Usage
if JavaScriptCompiler.js_exists?("main") do
js = JavaScriptCompiler.get_compiled_js("main")
else
JavaScriptCompiler.compile_and_store("main")
end
The AssetsState type a lazy hook's module is stored under, beside its site's bundle, from the
hash its file is named by: lazy_js:<hash>, with brotli_lazy_js:<hash> and
gzip_lazy_js:<hash> for its compressed copies.
The bundles the JS hook hook_id was left out of when they were last built, each with the
reason, sorted by bundle: [{"localhost", ~s(Could not resolve "echarts" (globalChart.js:28))}].
A bundle is a site's name, or "__shared_base__" for the global hooks every site without hooks
of its own shares. A global hook is built into every site's bundle, so it can be left out of
several.
Refreshes JS for a site (recompiles and updates).
Convenience function that recompiles JavaScript for a site and updates the cache. This is useful when JavaScript hooks or configurations change.
Parameters
site_name- The site identifier to refresh JavaScript for
Returns
{:ok, compiled_js}- On successful recompilation{:error, reason}- On compilation failure
Usage
# Refresh after updating JavaScript hooks
case JavaScriptCompiler.refresh_js("main") do
{:ok, js} ->
Logger.info("JavaScript refreshed successfully")
{:error, reason} ->
Logger.error("Failed to refresh: #{reason}")
endWhen to Use
- After updating JavaScript hooks in database
- When site configuration changes
- During development when app.js changes
- For periodic cache invalidation