# `MishkaCmsCore.Runtime.Compilers.JavaScriptCompiler`
[🔗](https://github.com/mishka-group/mishka_cms_core/blob/v0.0.1/lib/mishka_cms_core/runtime/compilers/java_script_compiler.ex#L5)

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

1. **Prepare Environment**: Creates temporary directories and asset structure
2. **Generate Runtime Hooks**: Fetches JS hooks from database and creates component files
3. **Ensure Colocated Hooks**: Sets up Phoenix colocated JavaScript integration
4. **Create Entry File**: Combines app.js with runtime components
5. **Execute ESBuild**: Bundles and minifies JavaScript using ESBuild
6. **Compress Assets**: Creates Brotli and Gzip versions for optimal delivery
7. **Store Results**: Caches compiled JS in AssetsState for fast serving
8. **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/1` and sent as `runtime: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

# `browser_name`

```elixir
@spec browser_name(map(), String.t() | nil) :: String.t()
```

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.

# `compile`

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

1. Creates temporary directories for build process
2. Generates runtime hooks from database-stored JavaScript
3. Ensures Phoenix colocated hooks are available
4. Executes ESBuild bundling with optimizations
5. 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

# `compile_and_store`

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

# `compile_and_store_shared_base`

Compiles + stores the shared base JS (`app.js` + global hooks only) under
`__shared_base__`. Built once and reused by every global-only site.

# `delete_compiled_js`

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

- `:ok` on 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")

# `delete_shared_base_js`

Drops the cached shared base JS (all formats), and the lazy hooks' modules it loads.

# `ensure_installed`

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.

# `get_compiled_js`

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>

# `get_or_compile_shared_base`

Cached shared base JS, compiling + caching it on first use.

# `js_exists?`

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

- `true` if compiled JavaScript exists in cache
- `false` if 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

# `lazy_type`

```elixir
@spec lazy_type(String.t()) :: String.t()
```

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.

# `left_out`

```elixir
@spec left_out(String.t()) :: [{String.t(), String.t()}]
```

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.

# `refresh_js`

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}")
    end

## When to Use

- After updating JavaScript hooks in database
- When site configuration changes
- During development when app.js changes
- For periodic cache invalidation

---

*Consult [api-reference.md](api-reference.md) for complete listing*
