@stonecrop/stonecrop

Schema-driven framework with XState workflows and HST state management
26,118 13
Install
npm install @stonecrop/stonecrop
Latest Version:0.55.0
Vue:3
License:MIT
Last Updated:Oct 22, 2026
Links: GitHub  ·  npm
Maintainer: knuckledown

Stonecrop

This package is under active development / design.

Features

  • Schema-Driven Relationships: links on doctype schemas declare relationships with cardinality and direction
  • Hierarchical State Tree (HST): Advanced state management with tree navigation
  • Operation Log: Global undo/redo with time-travel debugging, automatic FSM transition tracking, and action execution tracking
  • Action Tracking: Audit trail for stateless action executions (print, email, archive, etc.)
  • Field Triggers: Event-driven field actions integrated with XState

Installation & Usage

Vue Plugin Installation

import { createApp } from 'vue'
import Stonecrop, { Stonecrop as StonecropClass } from '@stonecrop/stonecrop'
import { StonecropClient } from '@stonecrop/graphql-client'
import router from './router'

const app = createApp(App)

// Install the Stonecrop plugin
app.use(Stonecrop, {
  router,

  // Run `onRouterInitialized` once the router is ready (defaults to false).
  autoInitializeRouter: true,

  // Lazy-load doctype metadata from your API. Called with the doctype slug you name in
  // `useStonecrop({ doctype: 'task' })`, as { path: '/task', segments: ['task'] }.
  getMeta: async ({ segments }) => {
    return await fetchDoctypeMeta(segments[0])
  },

  // Wire up the client after plugin initialization.
  // The callback receives registry and stonecrop instances directly.
  onRouterInitialized: (registry, stonecrop) => {
    const client = new StonecropClient({
      endpoint: 'http://localhost:4000/graphql',
      headers: { Authorization: `Bearer ${token}` },
      registry: buildMetaMap(registry),
    })
    stonecrop.setClient(client)
  },
})

Accessing Stonecrop Outside Vue Components

Inside a component, use useStonecrop(). Outside a component (e.g., workflow action handlers, utilities), keep the instance the plugin hands you at install time — it is passed to onRouterInitialized along with the Registry:

import StonecropPlugin, { type Stonecrop } from '@stonecrop/stonecrop'

export let stonecrop: Stonecrop | undefined

app.use(StonecropPlugin, {
  router,
  autoInitializeRouter: true,
  onRouterInitialized: (_registry, instance) => {
    stonecrop = instance
  },
})

// In a workflow action handler or non-component utility:
if (stonecrop) {
  const payload = stonecrop.collectRecordPayload(doctype, recordId)
  // ...
}

Building the DoctypeMeta Map

StonecropClient expects a Map<string, DoctypeMeta>, but the Registry stores Doctype instances. Convert between them:

import type { DoctypeMeta } from '@stonecrop/schema'
import type { Registry, Doctype } from '@stonecrop/stonecrop'

function buildMetaMap(registry: Registry): Map<string, DoctypeMeta> {
  const metaMap = new Map<string, DoctypeMeta>()
  for (const [slug, doctype] of Object.entries(registry.registry)) {
    metaMap.set(slug, {
      name: doctype.doctype,
      slug,
      fields: doctype.getSchemaArray(),
      links: doctype.links || {},
    })
  }
  return metaMap
}

Plugin Options

Option Type Description
router Router Vue Router instance. Used to register a route per doctype, and by the desktop shell.
getMeta (ctx: RouteContext) => Doctype | Promise<Doctype> Lazy-loads metadata for a named doctype slug. ctx has path and segments.
components Record<string, Component> Additional Vue components to register globally.
autoInitializeRouter boolean Call onRouterInitialized automatically after mount. Default: false.
onRouterInitialized (registry, stonecrop) => void Callback invoked after plugin install + mount. Receives the Registry and Stonecrop instances.

Available Imports

// Default export - Vue plugin (install with app.use)
import StonecropPlugin from '@stonecrop/stonecrop'

// Named exports - utilities and classes
import {
  Stonecrop,       // Core orchestration class
  Registry,        // Doctype registry (singleton)
  Doctype,         // Doctype definition class
  useStonecrop,    // Vue composable — primary integration point
  HST,             // HST store class
  createHST,       // HST factory function
} from '@stonecrop/stonecrop'

Using the Composable

import { useStonecrop } from '@stonecrop/stonecrop'

export default {
  setup() {
    // Base mode — operation log only, no HST record loading
    const { stonecrop, operationLog } = useStonecrop()

    // HST mode — pass Doctype instance and optional recordId
    const { stonecrop, formData, provideHSTPath, handleHSTChange } = useStonecrop({
      doctype: myDoctype,
      recordId: 'record-123', // omit or pass undefined for new records
    })

    // HST mode with lazy-loading — pass string doctype slug
    // Automatically loads doctype via registry.getMeta if not in registry
    const { isLoading, error, resolvedDoctype, resolvedSchema, formData } = useStonecrop({
      doctype: 'plan',
      recordId: 'record-123',
    })

    // Access HST store
    const store = stonecrop.value?.getStore()

    // Work with records directly
    const record = stonecrop.value?.getRecordById('doctype', recordId)

    return { stonecrop, formData }
  }
}

String Doctype Lazy-Loading

When you pass a string doctype slug instead of a Doctype instance, useStonecrop will:

  1. Check if the doctype is already in the Registry
  2. If not, call registry.getMeta to lazy-load it
  3. Return isLoading, error, and resolvedDoctype refs for handling the async state

The same refs cover reading the record: isLoading is true while it is fetched, and a failed read sets error and leaves formData empty, never a blank record in its place. A record the server does not have fails with the code RECORD_NOT_FOUND.

const { isLoading, error, resolvedDoctype, resolvedSchema, formData } = useStonecrop({
  doctype: 'plan', // string slug - triggers lazy-loading
  recordId: '123',
})

// In your template:
// <div v-if="isLoading">Loading...</div>
// <div v-else-if="error">Error: {{ error.message }}</div>
// <AForm v-else :schema="resolvedSchema" v-model:data="formData" />

This pattern eliminates the timing mismatch when loading doctypes asynchronously in Nuxt plugins.

Design

A Doctype defines schema, links, workflow, and actions.

  • Schema describes the data model and field layout — used by AForm for rendering.
  • Links declare relationships to other doctypes with cardinality and direction (noneOrMany, atMostOne, etc.).
  • Workflow is an XState machine config expressing the states and transitions a record can go through.
  • Actions are an ordered map of named functions, triggered by field changes (lowercase keys) or FSM transitions (UPPERCASE keys).
  • Registry is the singleton catalog — all doctypes live here. Optional Vue Router integration allows automatic route creation per doctype.
  • useStonecrop() is the Vue composable that wires components to HST and provides formData, provideHSTPath, handleHSTChange, and the operation log API.

The data model is two operations: get data and run actions. There is no CRUD. Records change state through FSM transitions; those transitions have side effects (persistence, notifications, etc.) defined in action handlers registered by the application. The framework provides the pipeline; applications define what actions exist and what they do.

HST path structure:

doctype.recordId.fieldname        // e.g. plan.abc-123.title
doctype.recordId.nested.field     // deep nesting supported

Hierarchical State Tree (HST) Interface Requirements

Core Requirements

1. Data Structure Compatibility

  • Vue Reactive Objects: Must work seamlessly with reactive(), ref(), and computed() primitives
  • Pinia Store Integration: Compatible with both Options API and Composition API Pinia stores
  • Immutable Objects: Support for frozen/immutable configuration objects without breaking reactivity

2. Path-Based Addressing System

  • Dot Notation: Full support for dot-notation paths (e.g., "users.123.profile.settings")
  • Dynamic Paths: Support for programmatically generated path strings (particularly component to HST)

3. Hierarchical Navigation

  • Ancestor/Descendant Relationships: Maintain bidirectional ancestor-descendant references
  • Sibling Access: Efficient navigation between sibling nodes
  • Root Access: Always accessible reference to tree root from any node
  • Depth Tracking: Know the depth level of any node in the hierarchy
  • Breadcrumb Generation: Generate full path breadcrumbs for any node

Related Packages

@arckinteractive/noah-ui

Vue.js framework for web-artisans brought to you by Arck Interactive.

1,189 13
anakyn-ui

This is a simple UI package

477 0
heyui

UI Toolkit for Web, Vue2.0.

80,586 2,539
mcl-ui-framework

A Vue 3 UI framework with Element Plus and custom components

6,066
heyui.ext

UI Toolkit for Web, Vue3.0.夏悸增强bug修复扩展版本

2,679 1

Version History

Version Released License
0.55.0 MIT
0.54.0 MIT
0.53.0 MIT
0.52.0 MIT
0.51.0 MIT