afx-team / afx-team/evjs

Add framework-managed client runtime hooks without changing the createApp contract

Aperta
#16 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
Lingua principale
TypeScript
Stelle
23
Fork
6
Merge medio
10h 8m
PR unite (30g)
27

Descrizione

## Problem

EVJS plugins can currently modify build config, transform HTML, and emit metadata, but they cannot participate in the client entry/runtime lifecycle in a framework-managed way.

Some integrations need to:

- run code after `createApp()` creates the router/query client;
- decide whether a top-level `app.render()` should proceed in the current host environment;
- run code before/after render and unmount;
- provide standard entry lifecycle exports such as `bootstrap`, `mount`, and `unmount`;
- adjust router/app options before the app is first rendered.

Without a framework-managed hook, application code must import integration-specific client helpers. That leaks integration details into otherwise normal EVJS entries.

## Non-goals

- Do not let plugins replace or mutate the public `App` shape returned by `createApp()`.
- Do not expose arbitrary plugin-generated export factories to user code.
- Do not solve this with bundler aliases that silently replace `@evjs/client`.
- Do not add integration-specific concepts to EVJS core.

## Required Contract

Add a generic client runtime contribution mechanism with fixed, framework-owned boundaries.

### 1. Build-time plugin contribution

A plugin may contribute browser runtime modules by descriptor only:

```ts
interface ClientRuntimeDescriptor {
/** Stable name for diagnostics and ordering. */
name: string;
/** Browser-side module specifier. */
module: string;
/** Lower runs earlier. Defaults to 0. */
order?: number;
}

interface EvPluginHooks {
clientRuntime?(): ClientRuntimeDescriptor[] | ClientRuntimeDescriptor | void;
}
```

EVJS should generate a virtual client runtime registry from these descriptors. The descriptor is declarative; it does not expose app instances or lifecycle factories to user code.

### 2. Runtime module contract

Each contributed module exports a fixed runtime object:

```ts
export interface ClientRuntimeContribution {
name: string;

/** Called after EVJS creates the app. Must not replace the app object. */
onAppCreated?(ctx: AppCreatedContext): void | Promise;

/** Called before router/app options are finalized. */
resolveAppOptions?(ctx: AppOptionsContext): Partial | void;

/** Return false to skip this render call. */
beforeRender?(ctx: RenderContext): false | void | Promise;

afterRender?(ctx: RenderContext): void | Promise;

beforeUnmount?(ctx: AppRuntimeContext): void | Promise;
afterUnmount?(ctx: AppRuntimeContext): void | Promise;

/** Fixed lifecycle slots only. EVJS owns how they become entry exports. */
lifecycles?: Partial>;
}
```

Important constraints:

- `ctx.app` is the stable EVJS `App` object and is read-only from the plugin contract perspective.
- Hooks can observe the app and call documented app methods, but they cannot return a replacement app.
- Lifecycle names are fixed. Plugins cannot create arbitrary entry exports.
- Shared runtime data, such as host mount props, must live in an EVJS-owned runtime context, not as ad-hoc fields attached to `app`.

### 3. Framework-managed entry lifecycle exports

When any runtime contribution provides `lifecycles`, EVJS should generate or wrap the client entry so the final bundle exports the fixed lifecycle names:

```ts
export const bootstrap = ...;
export const mount = ...;
export const unmount = ...;
```

These exports are consumed by host environments. User code should not need to import or call runtime export factories.

If user code already exports one of these lifecycle names, EVJS should fail with a clear diagnostic instead of silently overriding it.

### 4. Render behavior

`app.render(container)` should execute runtime `beforeRender` hooks in order. If any hook returns `false`, EVJS skips the actual React render for that call and reports the responsible runtime name in debug diagnostics.

This enables host-controlled environments to prevent eager standalone rendering while still allowing the generated `mount` lifecycle to render later through the same EVJS app contract.

### 5. App contract stability

The public app returned by `createApp()` remains stable:

```ts
interface App {
router: TRouter;
queryClient: QueryClient;
render(container: string | HTMLElement): void;
unmount(): void;
}
```

Plugins must not add required user-facing fields to this object. If an integration needs user-facing APIs, those APIs must be exported explicitly from a documented module, not hidden on `app`.

## Expected User Experience

Application code can keep the normal EVJS client import:

```ts
import { createApp } from '@evjs/client';

const app = createApp({ routeTree, queryClient });

app.render('#app');

export type AppRouter = typeof app.router;
```

Integration-specific behavior comes from `ev.config.ts` plugin configuration and generated EVJS runtime wiring, not from replacing `createApp` imports in application code.

## Acceptance Criteria

- A plugin can contribute an ordered client runtime module through `EvPluginHooks.clientRuntime()`.
- EVJS generates a runtime registry module for contributed client runtime modules.
- `createApp()` runs `resolveAppOptions` and `onAppCreated` without changing the public app shape.
- `app.render()` runs `beforeRender` / `afterRender`; `beforeRender === false` skips the underlying render call.
- `app.unmount()` runs `beforeUnmount` / `afterUnmount`.
- EVJS can generate fixed entry lifecycle exports `bootstrap`, `mount`, and `unmount` from runtime contributions.
- Duplicate lifecycle exports from user code produce a clear build-time error.
- TypeScript users can still use `typeof app.router` exactly as they do today.
- Add tests for runtime ordering, render skipping, lifecycle export generation, duplicate export diagnostics, and unchanged app public shape.

## Related

Focused follow-up to #13.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.