Docs/Bundle Generation

Bundle Generation

Elegance runs on a model of allowing server-side and client-side code to be written in one page.

Much like the idea of writing JS in HTML with JSX, and CSS in HTML with tailwind, we believe having your logic in 1 localized place helps developers reason about code better, and makes development less tangled and faster.

Boundaries

There's a few places you can't run server-side code, as they're considered seeding points for the page's bundle.

Within a component:

The view, onMount, onUnmount, and onNavigate methods are considered client only; but init can (by design) run server-side code, and is always guaranteed to be stripped from the bundle.

The atoms property is also considered client-side; so initializing an atom's using server-side code isn't allowed. Instead you should use the init method.

onPageLoad

The onPageLoad callback is a no-op on the server; because it's designed to run in the browser after hydration. Any transitively referenced variable within will be bundled.

on* Events

Values references within event handlers are required for the event handler (like onClick) to run, and thus need to be bundled.

If you'd like to run server-side code within your event handler, consider server-actions.

Safety

We recognise DCE isn't perfect and that developers make mistakes. If you really want to be sure that something doesn't end up in the client bundle, you can use directives directly above the definition:

`//!no-bundle`

ts
//!no-bundle
const ENV_SECRET = "iLikeCats123";

Marks the next declaration as server-only. If the client bundle ever reaches it, the build fails loudly with a "Server Only Error".

`//!no-bundle-file`

ts
//!no-bundle-file

Marks the entire file as server-only. If any reachable code comes from that file, the build fails with a "Server Only File" error. This is a file-level guard in addition to the per-declaration //!no-bundle directive.

`//!allow-bundling`

ts
//!allow-bundling
import { someFunc } from "some-package";

Above an import that must survive into the client bundle. Imports that get inlined disappear entirely, so this directive is rarely needed — use it only when a named import is reachable and needs to be preserved.

`//!force-bundling`

ts
//!force-bundling
import "some-package";

Above a side-effect import that must be kept. Side-effect imports are normally dropped by DCE unless marked with this directive.

If these are references you did not intend to mark server-only, you can remove the directive flag above their declarations. If that is not the case, remove the reference that cause their inclusion.

<div class="error-example"> ⚠️ Server Only Error

The following files are server-only but were reached by the client bundle:

• some/file.ts at <fn>() (/path/to/file:10:15)

The inclusion of server-only code in client-side bundles is almost always unintentional.

If these are references you did not intend to mark server-only, you can remove the //!no-bundle-file or //!no-bundle flag above their declarations. If that is not the case, remove the reference that cause their inclusion. </div>

Elegance also uses glob pattern matching (*, **, ?, [...]) in all directive and config declarations. Paths are matched using minimal glob semantics similar to bash globbing.

Splitting

Elegance uses esbuild for the generation of bundles, and so splitting across shared imported modules is automatically enabled for both dev and prod builds.

If you have two pages, A & B:

tsx
/pages/lib.ts
const counter = atom(0);

export function increment() {
    counter.value++;
    
    console.log(counter.value)
}

 /pages/a/page.tsx
import { increment } from "../lib";

export default function page() {
    return <div>
        <button onClick={() => increment()}>
            Increment
        </button>
    </div>
}

 /pages/b/page.tsx
import { increment } from "../lib";

export default function page() {
    return <div>
        <button onClick={() => increment()}>
            Increment
        </button>
    </div>
}

Because of the ESM cache, when both modules import the shared chunk, counter remains the same, and the state is maintained not only on other pages, but upon returning back to the same page, the values remain.

Config-based bundling

Add a bundling section to elegance.config.ts:

ts
bundling: {
    include: [],       // glob paths to force-bundle into client
    noBundle: [],      // glob paths to never include in client bundle
}

Paths in include will be inlined into the client bundle (behave as if //!allow-bundling was above them).

Paths in noBundle will never be included in the client bundle (behave as if //!no-bundle was above them). Reaching a banned path from client code causes a build error.

Packages can also self-declare via "elegance": { "bundle": [...], "no-bundle": [...] } in their package.json. The project-level config takes precedence over package declarations, and glob patterns (*, **, ?, [...]) are supported in all declarations.