@cratis/scene

中文文档
0.342.1发布于 2026/11/11MITTypeScriptES Module零依赖近 30 天下载 7.5万

Cratis Scene: renders Studio prototypes as thumbnails and previews.

原始使用文档
文档目录9 节
  1. Serialized prototype previews
  2. Layout preview DTOs
  3. Component class — how it works
  4. Why extends ExternalComponent?
  5. How serialization works
  6. The id bridge
  7. The _derivedTypeId own property
  8. The type key
  9. Subclass authoring

Scene

TypeScript models for the Studio's low-code canvas: UIElement hierarchy, ExternalComponent, and the prototype surface component system.

Serialized prototype previews

PrototypeThumbnail accepts both live IComponent instances and raw UIElement data from JSON documents. At render time Scene uses the element's library-qualified _derivedTypeId (or componentName) to construct a registered component and copy its saved state, recursively hydrating element arrays in all named slots without changing the source document. Without a registered library, it renders a positioned, sized placeholder rather than an empty prototype. Library registration does not trigger a React rerender by itself; rerender the thumbnail after loading bundles to replace placeholders with actual components. Without an open action the thumbnail is a named, non-focusable preview; with one it remains an editor-opening button. The board/document reader does not load bundles or depend on Studio Core.

Layout preview DTOs

The six StudioLayout.* types (Stack, Row, Grid, Container, Wrap, and Canvas) are backend preview DTOs. They derive from the existing FrameworkElement/UIElement hierarchy and use the same on-wire ComponentName, inherited Properties, and Slots shape as ExternalComponent, so generic board previews can round-trip their nested children. They deliberately do not derive from ExternalComponent: the current derived-type converter treats a base with registered subclasses as a discriminator family and then cannot deserialize a generic externalComponent itself. This compatibility boundary avoids changing Fundamentals or breaking existing generic component, Dialog, and ConfirmDialog data.

A SceneDocument is the sole editable model. Layout DTOs are produced only for the legacy board-preview projection; they are not a second Scene model, editor, resolver, or persistence authority.

Component class — how it works

Component (in Component.ts) is the abstract base class for every prototype surface component (e.g. the 89 PrimeReact wrappers under Source/PrimeReact/). It extends ExternalComponent so instances are proper UIElement subtypes that flow through the command pipeline without any manual wire-format conversion.

Why extends ExternalComponent?

Commands that carry elements use UIElement[] as their type on both the C# and TypeScript sides. Before this design, Component was a separate class that implemented IComponent and had to be converted to a wire-format plain object before assignment (toWireElement). That conversion was a callsite smell that hid serialization failures and required every consumer to know about it.

Now Component extends ExternalComponent implements IComponent. An InputText instance IS a UIElement — pass it directly:

command.elements = updatedElements as unknown as typeof command.elements;

The as unknown as cast is still needed because TypeScript sees UIElement[] (with id: Guid) while the editor's PrototypeSurfaceElement type has id: string. The runtime values are correct; only the declared types diverge at this boundary.

How serialization works

The Fundamentals JsonSerializer walks Object.getOwnPropertyNames(instance) and serializes each own property. The design of Component ensures only JSON-safe values appear as own properties:

WhatOwn property?Why
idYes — string accessor via Object.definePropertyC# System.Text.Json parses a UUID string as Guid automatically
ZIndex, anchoring, width, height, name, properties, slots, componentNameYes — class fields set in initialize()All are plain values; safely serialized
_derivedTypeIdYes — set explicitly in initialize()Tells Fundamentals to deserialize as ExternalComponent on the server
toolbarIconNo — prototype getterReact-icons components are regular functions with a prototype.constructor self-reference; serializing them causes infinite recursion. Being a prototype getter keeps them out of Object.getOwnPropertyNames entirely
RenderPrototypeNo — abstract prototype methodSame reason; a method on the prototype is never an own property of the instance
label, position, size, children, typeNo — prototype gettersComputed bridges; not serializable data

The id bridge

SceneObject declares id: Guid (via @field(Guid)). IComponent declares id: string. The constructor replaces the class-field own property with a plain-string accessor:

let _id = '';
Object.defineProperty(this, 'id', {
    get() { return _id; },
    set(value: string) { _id = String(value ?? ''); },
    enumerable: true,
    configurable: true,
});

The serializer reads the string UUID; C# parses it as Guid. Editor code that compares IDs uses string equality throughout.

The _derivedTypeId own property

DerivedType.get(constructor) uses Reflect.getOwnMetadata, which does not traverse the prototype chain. The @derivedType('externalComponent') decorator is on ExternalComponent, so DerivedType.get(InputText) returns undefined. Without the explicit own property the JSON output would have no _derivedTypeId and the C# deserializer would not know to construct an ExternalComponent.

initialize() sets it directly:

(this as unknown as Record<string, unknown>)['_derivedTypeId'] = 'externalComponent';

The type key

After initialize(), componentName is updated to the full library-qualified key ('PrimeReact.InputText'). Before initialize() the subclass class field holds the short name ('InputText'). The type getter always returns the fully qualified key by splitting on . and re-joining with library:

get type(): string {
    const cn = this.componentName ?? '';
    const shortName = cn.split('.').pop() ?? cn;
    return this.library ? `${this.library}.${shortName}` : shortName;
}

Subclass authoring

Implement these members:

export default class InputText extends Component {
    readonly componentName = 'InputText'; // short name; initialize() stores the full key
    readonly category = 'Form';
    readonly library = 'PrimeReact';
    readonly initialSize = { width: 220, height: 42 };
    readonly resizeBehavior = 'horizontal' as const;

    // MUST be a getter — not a class field.
    // Class fields are own instance properties and cause infinite recursion in Fundamentals' serializer.
    override get toolbarIcon() { return FaKeyboard; }

    RenderPrototype({ label }: PrototypeComponentRenderProps): ReactNode { ... }
}

toolbarIcon must be an override get. A class field (readonly toolbarIcon = FaKeyboard) makes FaKeyboard an own instance property. Fundamentals' serializer then calls Object.getOwnPropertyNames(FaKeyboard) which includes prototype, whose constructor points back to FaKeyboard, causing unbounded recursion and a RangeError.

版本与发布记录

@cratis/scene 共发布 553 个版本,首次发布于 2026/10/01;过去 12 个月发布了 553 个版本。

版本发布日期距今
0.342.1latest2026/11/11今天
0.342.02026/11/11今天
0.341.12026/11/11今天
0.341.02026/11/11今天
0.340.02026/11/11今天
0.339.32026/11/10今天

关于 @cratis/scene 的常见问题

@cratis/scene 是什么?

Cratis Scene: renders Studio prototypes as thumbnails and previews.

如何安装 @cratis/scene?

在项目目录运行 npm install @cratis/scene,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(^19.3.0)、@cratis/fundamentals(^7.22.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@cratis/scene 支持 TypeScript 吗?

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

@cratis/scene 支持 ESM 还是 CommonJS?

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

@cratis/scene 可以商用吗?

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

@cratis/scene 的下载量有多少?

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

@cratis/scene 还在维护吗?

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