@ecopages/radiant

中文文档
0.8.4发布于 2026/11/13MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 6524
原始使用文档
文档目录12 节
  1. Installation
  2. RadiantElement Mental Model
  3. Counter Example
  4. When To Call update()
  5. Reactive JSX Bindings
  6. SSR And Hydration
  7. Event Handling
  8. How JSX Event Binding Works
  9. @onEvent(...)
  10. @event(...)
  11. Development Warnings
  12. Public Entry Points

Radiant

Radiant is a light-DOM platform for custom elements and DOM-attached controllers.

It keeps browser primitives visible instead of wrapping them in a synthetic component model. You work with real custom elements, real DOM events, real attributes, and real light-DOM children. Use RadiantElement when you want reactive fields, JSX-backed rendering, SSR host serialization, and hydration on a real custom element. Use RadiantController when you want behavior attached to existing DOM instead of defining a custom element.

Radiant deliberately renders into light DOM only; it has no shadow render mode. That keeps styling, DOM inspection, authored child content, and ARIA id references between host-owned and authored nodes simple, while giving up the encapsulation that conventional custom-element guidance usually prefers.

For the full docs site, see radiant.ecopages.app.

Installation

@ecopages/radiant is the core package. @ecopages/jsx provides TSX rendering, and @ecopages/signals provides the renderer-agnostic reactive layer underneath Radiant.

@ecopages/radiant depends on @ecopages/signals directly and declares @ecopages/jsx as a peer dependency. Installing radiant brings signals transitively, which also satisfies jsx's peer dependency on signals.

For the standard Radiant setup, install radiant and jsx:

bun add @ecopages/radiant @ecopages/jsx

Application code does not need to import JSX helpers or Signals primitives directly for every feature, but the current published Radiant surface expects @ecopages/jsx to be installed alongside it. Install @ecopages/signals explicitly only when you want the standalone reactive layer outside Radiant, or when using @ecopages/jsx without radiant.

RadiantElement Mental Model

RadiantElement is the structured base class for reactive custom elements.

@customElement(name, options) registers autonomous custom elements only. Passing a defined options.extends value to define a customized built-in throws a TypeError at class definition time, and extends is excluded from the TypeScript options type.

options.hostAttributes gives a RadiantElement host fixed attributes, such as { role: 'complementary' }. Server rendering writes them while it prepares the host, so they reach server output, and the client writes them at the start of every connect, before onConnected(). An attribute already on the host is never overwritten. A subclass inherits them and overrides them by name. State-dependent attributes stay with @bindTo and @onUpdated.

  • render() returns the current JSX view.
  • First connect automatically chooses between hydration and a fresh client render.
  • update() runs the update cycle now: batched @onUpdated callbacks, the render when the host overrides render(), then updated(). It works on every host, so it also flushes @onUpdated synchronously on hosts without render().
  • requestUpdate() schedules that cycle in a microtask and coalesces repeated requests. updateComplete resolves when the cycle finishes and rejects when an @onUpdated callback, render, or updated() in it throws. A failed cycle discards its remaining queued work; a later write starts a fresh cycle.
  • Hosts observe their reactive members only while connected. Changes made before connect or while detached are reported on the next connect, so a shared @signal source never keeps a removed host alive. Server rendering never connects hosts; it drains pending @onUpdated callbacks during host preparation and never calls updated().
  • @prop(...), @state, and @signal(...) define reactive members. A @prop attribute defaults to the kebab-case property name (minWidth maps to min-width); @attr uses the same conversion for its source attribute, with consecutive underscores collapsing to one hyphen. attribute on @prop and source on @attr override the default and preserve the explicit name. JSX continues to use the camelCase property. HTML lowercases attribute names, so choose a lowercase explicit name when consumers author it in HTML. Pass transform on @prop to override default converters (type: Array is JSON unless you supply fromAttribute / toAttribute). fromProperty normalizes JS/JSX writes. toAttribute returning null or '' omits the reflected attribute without changing the assigned property.
  • The declared field type is the source of truth for @prop(...), @attr(...), @signal(...), @state, @bindTo(...), @query(...), @event(...) and the context decorators: options that carry a value (defaultValue, initial, transform, converter, type, and source on @signal) must agree with it, and under standard decorators a mismatch is a compile error on the decorator. @prop({ type: Number, defaultValue: 0 }) label: string does not compile, and @attr({ defaultValue: 'a' }) mode: 'a' | 'b' does. A single-element @query(...) field is T | null (T[] with all: true), so guard each use. Write a decorated field with an initializer, ? or !: declare and accessor fields do not compile. Legacy decorators (experimentalDecorators: true) never receive the field type, so none of these checks run there and a callback such as @bindTo's map sees unknown unless you pass the whole field type, undefined included, as a type argument (@bindTo<number>(...), @bindTo<WritableSignal<number>>(...) on a @signal field). The rules per decorator, the type constant and parsers are in Typing Decorated Fields.
  • An authored attribute stays on the host after upgrade, reflected or not: reflect only controls writing property changes back, so a non-reflected attribute is read at upgrade and not kept in sync afterwards.
  • Upgrade safety: a value assigned to a host before its tag is defined becomes an own property of the not-yet-upgraded element. @prop members adopt it, and it wins over an authored attribute, so consumers never need to defer an element definition to avoid losing input. @state and @signal are internal members, not public input: they keep their initializer and discard such a value. In development, a host that upgrades with one warns once per tag and member, naming the member and pointing to @prop; production builds drop the warning and setDevWarningsEnabled(false) from @ecopages/jsx/jsx-dev-runtime silences it. A pre-upgrade assignment also shadows a class accessor (hand-written get/set, an accessor field, or a library accessor decorator such as MobX @observable accessor), so the accessor never sees it; that warns once per tag and member the same way; to recover after upgrade, save the value, delete the own property and assign it again so it goes through the setter. Keep library state in a store passed through @prop or context, not in accessor members on the host.
  • @prop({ attribute: false }) makes a prop property-only: no observed attribute, an authored attribute of that name is ignored, it is never reflected and never serialized by server rendering. Use it for live objects such as class instances, which would otherwise leak their fields into the markup, lose their methods and, if cyclic, throw during SSR; reflect: true with it throws. Serialized props are public markup, so keep private or large data out of a prop that keeps its attribute. In development, server rendering warns once per tag and prop when a prop that keeps its attribute holds a class instance, function or cycle.
  • Reflected properties serialize the current value on each write. A synchronous callback (@bindTo, registerUpdateCallback) that normalizes the assignment is reflected too.
  • @bindTo(...) copies a reactive field onto existing DOM when the host does not own a render() tree.
  • @onUpdated(...) runs once for members changed in the same batch, before the render commits. If its callback changes a watched member, it can run again in the same cycle until state settles. Use it for procedures and for asking a render-owning host to update() / requestUpdate() when the view structure must change. updated() is for work that needs the committed DOM.
  • this.bindings.key, this.$.key, and this.bind('key') expose stable JSX bindings for reactive members.
  • If render() is omitted, authored light-DOM children pass through unchanged. Copy fields onto that DOM with @bindTo(...).
  • registerConnectedCallback() is a pre-sync hook: it runs synchronously when connection starts, before first-connect attribute catch-up. Use it only for setup that does not read authored property values. onConnected() runs after every connection, once attribute catch-up and (when render() is overridden) the initial hydrate/update have finished; use it instead of connectedCallback + queueMicrotask(sync) when work needs the synchronized host.
  • FormAssociatedElement extends RadiantElement for native form controls. Each concrete subclass inherits the static FACE flag and owns one ElementInternals; implement formValue() and restoreFormState(). Override formState() when submission can be null before a name is assigned or uses multi-entry FormData, so reset keeps the authored value. The first form value sync, normally in the first connect cycle, records that reset state.
  • {this.slotted} places a render-owning host's children, and {this.slotted ?? fallback} gives a fallback, since it is null when there are none. A view that renders <x-card>{children}</x-card> hands a Children Value to the host, and only the host's renderer touches the nodes. Named regions are properties (<x-card header={<h2 />}> rendered with {this.header}). Authored Children are captured once, at first render; nodes appended later are not slotted. this.slottedElements reads the top-level elements the content rendered to. <slot> is an ordinary element. See Slots.

The key distinction is this:

  • Use @bindTo when the host keeps authored DOM and a field should appear as an attribute, property, or text.
  • Use bindings such as this.$.count when the host owns render(), the shape of the view stays the same, and only child values need to change.
  • Use update() or requestUpdate() when a reactive change affects the structure of the JSX tree.

Signal and store reads performed directly inside render() also participate in rerender invalidation, so shared reactive data can drive RadiantElement views without an extra wrapper layer.

Counter Example

This counter shows the intended RadiantElement style for stable templates: public props stay explicit, internal state stays local, and bound child values update without forcing a full rerender of the whole template.

/** @jsxImportSource @ecopages/jsx */

import type { JSX_PROPS } from '@ecopages/jsx';
import { RadiantElement, customElement, prop, state } from '@ecopages/radiant';

type CounterCardBindings = {
    count: number;
    label: string;
};

type CounterCardProps = {
    label?: string;
};

@customElement('counter-card')
export class CounterCard extends RadiantElement<CounterCardBindings> {
    declare readonly [JSX_PROPS]?: CounterCardProps;

    @prop({ type: String, defaultValue: 'Clicks' }) label!: string;
    @state count = 0;

    private readonly increment = () => {
        this.count += 1;
    };

    override render() {
        return (
            <section>
                <h2>{this.$.label}</h2>
                <p>Count: {this.$.count}</p>
                <button type="button" on:click={this.increment}>
                    Increment
                </button>
            </section>
        );
    }
}

<CounterCard label="Clicks" />;

How this works:

  • @prop(...) makes label part of the component's public reactive property and attribute surface.
  • @state is internal mutable state.
  • this.$.label and this.$.count are cached JSX bindings, so those text positions can update directly when the underlying reactive members change.
  • This example does not call update() because the view structure is stable. Only the bound child values change.
  • JSX_PROPS declares a public type that is narrower than the internal binding shape. That is usually the right split.

If the same type truly represents both internal bindings and public JSX attributes, you can reuse it for both. Otherwise, keep those contracts separate.

When To Call update()

Call update() or requestUpdate() when a reactive change affects which elements should exist, not just their child values.

import { RadiantElement, onUpdated, state } from '@ecopages/radiant';

export class ResultsPanel extends RadiantElement<{ expanded: boolean }> {
    @state expanded = false;

    @onUpdated('expanded')
    protected rerenderView(): void {
        this.requestUpdate();
    }
}

Use update() when you want the rerender immediately. Use requestUpdate() when several changes may happen in the same turn and one final rerender is enough.

Reactive JSX Bindings

Bindings are the non-obvious part of the API, and they are worth being explicit about.

RadiantElement<Bindings> takes a dedicated binding shape, not the full class type. That keeps the binding namespace limited to the reactive props or fields you want JSX to consume.

These three forms are equivalent:

  • this.bindings.count
  • this.$.count
  • this.bind('count')

They all resolve through the same cached binding object.

Use bindings when:

  • the rendered element structure stays the same
  • only text, attribute, or child-value positions need to change
  • you want the renderer to patch the owned child range instead of rerunning the whole component view

Use @onUpdated(...) with update() or requestUpdate() when the reactive change should rebuild the JSX tree.

SSR And Hydration

Prefer the server pipeline for host HTML:

  • import @ecopages/radiant/server/install-ssr-runtime once at server boot (shim + ALS scope adapters)
  • renderComponent(...) / renderComponentToString(...) from @ecopages/radiant/server/render-component for adapters and fragments
  • renderRadiantElementHostToString(...) from @ecopages/radiant/server/radiant-element-ssr for lower-level host serialization
  • renderRadiantElementViewToString(host, options) from @ecopages/radiant/server/radiant-element-ssr serializes only the JSX view

There is no durable Element Host instance API named renderHostToString(). renderComponent() serializes each host once and wraps that markup as preview; it does not serialize the host a second time.

mode: 'hydrate' adds hydration markers for the component view and stamps each serialized host with a data-radiant-ssr attribute. A host reads that stamp once, at first connect, into isServerRendered. Use it, not hasHydrationMarkers, to tell whether a host's content came from the server: markers exist only where a view has bindings, and JSX children applied to a custom element as light DOM carry none. Plain-mode hosts are not stamped and report false; renderRadiantElementViewToString(host, options) returns the prepared view only, so a caller that writes the host tag itself must add SERVER_RENDERED_HOST_ATTRIBUTE (exported from @ecopages/radiant/server/radiant-element-ssr). The host removes the stamp once it has read it, as hydration markers are removed, so a connected host no longer carries it. A host inside server-rendered slotted content keeps the server's node, or comes from the slotted template with its stamp, so it reports true. A render-owning host in another host's view keeps its server node, state and focus when that parent hydrates, so it reports true too; one the parent's view rebuilds (its tag was defined only after the parent hydrated, or the server markup does not match the view) reports false. A page root with no binding of its own can still reconnect after such a nested host has hydrated or rendered itself, even if it updated since, when the page's static markup matches. First-connect hydration is explicit: SSR pages should import @ecopages/radiant/client/install-hydrator before loading component modules, or call installRadiantHydrator() from @ecopages/radiant/client/hydrator before custom elements upgrade. Without that client hydrator gate, SSR hosts fall back to a fresh client render on first connect.

Because Radiant's SSR environment shims window and document, global checks like typeof window !== 'undefined' or typeof document !== 'undefined' evaluate to true on the server. Guard top-level event listeners on document or window with !isServer from @ecopages/radiant/is-server so browser navigation and event listeners do not attach during SSR. This flag follows the package export condition, so it reads true under Node, Deno, Bun, Cloudflare Workers (workerd) and Vercel Edge (edge-light) even when a DOM shim is installed, and false in a browser bundle (the browser condition) and under any runtime that sets none of those conditions. Use a DOM capability check for code that depends on specific DOM methods.

createResource(...) starts from the Element Host client connect pass, after authored attributes synchronize. SSR serialization never starts resource fetches, regardless of the active package export condition. For server-rendered data, fetch before rendering and provide the value through initialize, prepareHost, or host attributes.

Server runtime setup, fragment rendering helpers, and SSR-specific import guidance live in src/server/README.md.

For the client lifecycle and hydration flow diagram, see src/core/README.md.

Event Handling

Radiant does not invent a synthetic event layer. JSX handlers and decorators work with the native browser event object directly.

Use thisWhen you wantRuntime shape
on:* in JSXThe normal event APIAuto-delegates a fixed allowlist of bubbling events and falls back to direct listeners otherwise
on-native:* in JSXExact element-level browser listener semanticsAlways calls addEventListener(...) on that element
@onEvent(...)Class-level listening from RadiantElementSupports selector, ref, window, and document targets
@event(...)A typed custom event emitter owned by the componentDispatches a real CustomEvent from the host element

How JSX Event Binding Works

on:* is the default JSX event API. For a fixed allowlist of bubbling events, Radiant attaches one listener per event type on the render root and dispatches to matched elements. For all other events, on:* attaches directly on the element.

Today the delegated allowlist is:

  • beforeinput
  • click
  • contextmenu
  • dblclick
  • focusin
  • focusout
  • input
  • keydown
  • keyup
  • mousedown
  • mouseout
  • mouseover
  • mouseup
  • pointerdown
  • pointerout
  • pointerover
  • pointerup
  • touchend
  • touchmove
  • touchstart

Use on-native:* when exact attachment semantics matter, such as when an ancestor may stop propagation before the delegated root listener runs or when you want to opt out of delegation for a supported bubbling event.

Events outside the allowlist, such as focus, blur, scroll, load, invalid, or dragstart, already attach directly when authored with on:*.

@onEvent(...)

At the class level, @onEvent(...) is the main decorator for incoming DOM events. It can listen to:

  • descendants that match a CSS selector
  • descendants marked with data-ref
  • the host itself (host: true), including non-bubbling events such as pointerenter and pointerleave
  • global window events
  • global document events
/** @jsxImportSource @ecopages/jsx */

import { RadiantElement, customElement, onEvent, state } from '@ecopages/radiant';

@customElement('keyboard-panel')
export class KeyboardPanel extends RadiantElement<{ lastKey: string }> {
    @state lastKey = 'none';

    @onEvent({ document: true, type: 'keydown' })
    protected onKeydown(event: KeyboardEvent): void {
        this.lastKey = event.key;
    }

    override render() {
        return <p>Last key: {this.$.lastKey}</p>;
    }
}

Selector- and ref-based @onEvent(...) handlers, and subscribeEvent(...) / subscribeEvents(...), match with Element.closest(...) from the event target, so a click on a nested icon still reaches a wrapping button[data-ref] or selector match. They still rely on bubbling, so use focusin and focusout instead of focus and blur for that pattern.

Each subscribeEvent(...) registration is independent, including duplicate type and selector pairs. Unsubscribing one listener does not remove another, and disconnect removes every remaining registration.

@event(...)

Outgoing component events use @event(...), which gives the class a typed EventEmitter. Calling .emit(detail) dispatches a real CustomEvent from the host element.

import type { EventEmitter } from '@ecopages/radiant/tools/event-emitter';
import { RadiantElement, customElement, event } from '@ecopages/radiant';

type SaveDetail = {
    id: string;
};

@customElement('save-button')
export class SaveButton extends RadiantElement {
    @event({ name: 'save-requested', bubbles: true, composed: true })
    saveRequested!: EventEmitter<SaveDetail>;

    requestSave(): void {
        this.saveRequested.emit({ id: 'draft-1' });
    }
}

The mental model stays simple:

  • DOM events come in through JSX handlers or @onEvent(...)
  • component events go out through @event(...)
  • both are ordinary browser events at runtime

Development Warnings

Radiant logs development warnings when a delegated @onEvent(...) listener uses a non-bubbling event type, such as focus, and when a tag is defined after a view rendered it. The warnings require both the Radiant process.env.NODE_ENV development gate and the shared @ecopages/jsx switch: setDevWarningsEnabled(false) silences both packages, while true or undefined enables them again. The published files keep the process.env.NODE_ENV check that gates them, so the app's build decides, not Radiant's: the warnings show when the bundler sets process.env.NODE_ENV to anything other than "production", and a production build removes them along with their text. What counts as a production build depends on the bundler: esbuild sets "production" for a browser-platform build with minification on, Vite for vite build unless NODE_ENV says otherwise, webpack for mode: "production", and Bun for --production. A page that loads the files without a bundler, where process does not exist, gets no warnings and no error.

The JSX hydration and DOM anchor warnings come from @ecopages/jsx and follow its own switch; see "Dev Warnings" in the JSX README.

Contributors: wrap a development warning in try { if (process.env.NODE_ENV !== 'production' && areJsxDevWarningsEnabled()) { ... } } catch {}. The try keeps a page without process from throwing, and keeping both checks inside it lets every bundler drop the block in production; see warnNonBubblingDelegatedEvent in src/helpers/create-event-listener.ts and setChildrenValue in src/core/render-runtime.ts. test/build/dist-dev-warnings.test.ts fails when a built dist file reads process.env.NODE_ENV outside that shape, and bundles an app against the dist for development, for production, and with the check left unreplaced. test/build/dist-dev-warnings.browser.test.ts runs a development and a production bundle of an app in Chromium and checks which warnings reach console.warn. Both run in their own node-dist and browser-dist vitest projects, whose globalSetup (test/build/dist-global-setup.ts) runs only when a test/build file is in the run. It rebuilds dist with build:lib when the content hash of build.ts, package.json, the two tsconfig files, scripts and src differs from the one recorded at the last build (node_modules/.cache/dist-source-hash), so a whitespace edit needs no manual build. Under CI it never rebuilds and only requires dist to exist; a failed rebuild points at pnpm --filter @ecopages/radiant build:lib.

Public Entry Points

These are the documented public import paths exposed by the package.

PathUse for
@ecopages/radiantMain client entrypoint. Re-exports RadiantElement, RadiantController, reactive JSX bindings, common decorators, and controller-registry helpers such as startControllers(...)
@ecopages/radiant/contextContext-related exports as a grouped entrypoint
@ecopages/radiant/context/create-contextCreating context keys
@ecopages/radiant/context/context-providerLow-level context provider class
@ecopages/radiant/context/consume-context@consumeContext(...) decorator
@ecopages/radiant/context/provide-context@provideContext(...) decorator
@ecopages/radiant/context/context-selector@contextSelector(...) decorator — bind a field to context
@ecopages/radiant/context/on-context-update@onContextUpdate(...) decorator — run a method on context change
@ecopages/radiant/context/eventsContext request / subscription event types
@ecopages/radiant/controller-registryController registration and activation helpers
@ecopages/radiant/core/radiant-elementNon-JSX reactive custom-element base
@ecopages/radiant/core/radiant-controllerDOM-attached controller base
@ecopages/radiant/form-associated-elementFormAssociatedElement base for hosts that list on native FormData
@ecopages/radiant/helpers/create-queryLow-level query helper for component fields
@ecopages/radiant/helpers/create-eventLow-level typed custom-event helper
@ecopages/radiant/helpers/create-event-listenerLow-level DOM event-listener helper
@ecopages/radiant/helpers/debounceDebounce helper without the decorator surface
@ecopages/radiant/is-serverBoolean flag (isServer) for branching between server and browser environments
@ecopages/radiant/client/hydratorExplicit client hydrator installer and status helpers for SSR pages
@ecopages/radiant/client/install-hydratorSide-effect entrypoint that enables first-connect hydration before component modules load
@ecopages/radiant/client/app-bootstrapprepareRadiantApp(...) app-shell bootstrap helpers
@ecopages/radiant/signals/host-resourceLow-level HostResource, createHostResource(...), and createResource(...) helpers
@ecopages/radiant/server/install-ssr-runtimeSide-effect server boot entry: light-DOM shim plus JSX SSR scope adapters
@ecopages/radiant/server/install-light-dom-shimSide-effect shim install only (prefer install-ssr-runtime for full Radiant SSR)
@ecopages/radiant/server/light-dom-shimMinimal SSR window and host-preparation helpers
@ecopages/radiant/server/radiant-element-ssrLower-level host serialization (renderRadiantElementHostToString defaults to plain mode; see src/server/README.md for the full export tiers)
@ecopages/radiant/server/render-componentCanonical component SSR helpers; default hydrate mode
@ecopages/radiant/server/render-controllerController-host SSR helpers and controller-specific host option types
@ecopages/radiant/server/project-rootProject-root resolution helper for server adapters
@ecopages/radiant/decorators/attr@attr(...)
@ecopages/radiant/decorators/bind-to@bindTo(...)
@ecopages/radiant/decorators/bound@bound
@ecopages/radiant/decorators/controller@controller(...)
@ecopages/radiant/decorators/custom-element@customElement(...)
@ecopages/radiant/decorators/debounce@debounce(...)
@ecopages/radiant/decorators/event@event(...)
@ecopages/radiant/decorators/on-event@onEvent(...)
@ecopages/radiant/decorators/on-updated@onUpdated(...)
@ecopages/radiant/decorators/prop@prop(...)
@ecopages/radiant/decorators/query@query(...)
@ecopages/radiant/decorators/signal@signal(...)
@ecopages/radiant/decorators/state@state
@ecopages/radiant/tools/escape-script-jsonSafe JSON-for-script serialization helper
@ecopages/radiant/tools/event-emitterLow-level EventEmitter helper

ValueFieldDecorator, BoundedFieldDecorator and the host types they name are exported from @ecopages/radiant (ContextHostLike and ContextUpdateMethod from @ecopages/radiant/context), so a library that exports a shared decorator, such as export const count = prop({ type: Number }), can emit declarations.

Prefer focused public subpaths when bundle size matters or when a module only needs one narrow runtime surface. The root entrypoint remains the ergonomic default for common app code, but context, registry, helper, server, signal-resource, and low-level tool imports are better taken from their explicit public subpaths.

For controller authoring, prefer a single import surface per module. If a file mixes RadiantController, @controller(...), @state, @attr(...), or startControllers(...), default to @ecopages/radiant so controller registration and startup come from the same entrypoint.

Important: context APIs, controller registry helpers, helper factories, server APIs, and low-level tools have explicit public subpaths. Prefer those focused paths for library and adapter code, or when a module intentionally wants only the registry runtime without the broader client surface; the root entrypoint remains the ergonomic app entrypoint for the common client surface.

For SSR-specific guidance and examples, see src/server/README.md.

版本与发布记录

@ecopages/radiant 共发布 69 个版本,首次发布于 2024/06/01;过去 12 个月发布了 59 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.8.4latest2026/11/13今天
0.8.32026/11/13今天
0.8.22026/11/085 天前
0.8.12026/11/085 天前
0.8.02026/11/085 天前
0.7.02026/11/076 天前

其他发布通道:alpha → 0.3.0-alpha.25,beta → 0.3.0-beta.8,rc → 0.3.0-rc.13。

关于 @ecopages/radiant 的常见问题

如何安装 @ecopages/radiant?

在项目目录运行 npm install @ecopages/radiant,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@ecopages/jsx(>=0.8.4)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@ecopages/radiant 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@ecopages/radiant 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@ecopages/radiant 可以商用吗?

@ecopages/radiant 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@ecopages/radiant 的下载量有多少?

根据 npm 官方下载统计,@ecopages/radiant 在 2026/10/13 – 2026/11/11 这 30 天内被下载 6,524 次,日均约 217 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@ecopages/radiant 还在维护吗?

最新版本 0.8.4 发布于 2026/11/13(今天);至今共发布 69 个版本,其中过去 12 个月发布了 59 个;首次发布于 2024/06/01。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。