@stonecrop/nuxt 0.10.2 → 0.10.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,10 +7,10 @@ The official Nuxt module for Stonecrop - a schema-driven UI framework with event
7
7
 
8
8
  ## What is Stonecrop?
9
9
 
10
- Stonecrop is a **schema-driven UI framework** that generates forms, tables, and workflows from JSON schemas. Instead of manually creating CRUD interfaces for every data model, you define your data structure once and Stonecrop handles the UI generation, state management, and validation automatically.
10
+ Stonecrop is a **schema-driven UI framework** that generates forms, tables, and workflows from JSON schemas. You define your data structure once and Stonecrop handles UI generation, state management, and validation.
11
11
 
12
12
  **Key Benefits:**
13
- - **Schema-Driven**: Define data models in JSON, get full CRUD interfaces automatically
13
+ - **Schema-Driven**: Define data models in JSON; form and table rendering follows automatically
14
14
  - **HST State Management**: Hierarchical State Tree for complex, nested application state
15
15
  - **FSM Workflows**: XState-powered finite state machines for predictable business logic
16
16
  - **Nuxt Native**: First-class integration with Nuxt 4's architecture
@@ -100,6 +100,27 @@ Create a JSON schema in `/doctypes/task.json`:
100
100
 
101
101
  The module picks up this file and, if `pageComponent` is configured, registers a route at the doctype's `slug` value (or `task` if no slug is set), passing the parsed schema into `route.meta`.
102
102
 
103
+ ### Wire Up Your Data Client
104
+
105
+ After installing the module, add a client-side plugin to connect your data transport. The `useStonecropRegistry()` composable gives you a stable API for this — no need to reach into `globalProperties` directly:
106
+
107
+ ```typescript
108
+ // app/plugins/stonecrop.client.ts
109
+ import { StonecropClient } from '@stonecrop/graphql-client'
110
+
111
+ export default defineNuxtPlugin(() => {
112
+ const client = new StonecropClient({ endpoint: '/graphql' })
113
+ const { setMeta, setFetchRecord, setFetchRecords } = useStonecropRegistry()
114
+
115
+ // Map the route path/segments to a doctype name, then delegate to your client
116
+ setMeta(({ segments }) => client.getMeta({ doctype: segments[0] }))
117
+ setFetchRecord((doctype, id) => client.getRecord(doctype, id))
118
+ setFetchRecords((doctype) => client.getRecords(doctype))
119
+ })
120
+ ```
121
+
122
+ This wires `useStonecrop()`'s automatic record loading to your GraphQL (or any other) backend. Without this step, `useStonecrop({ doctype, recordId })` falls back to a REST fetch stub that may not exist in your app.
123
+
103
124
  ### Use the Stonecrop Composable
104
125
 
105
126
  In your page or component:
@@ -108,19 +129,20 @@ In your page or component:
108
129
  <script setup lang="ts">
109
130
  import taskDoctype from '~/doctypes/task.json'
110
131
 
111
- // HST-reactive form setup
132
+ // HST-reactive form setup — pass doctype + recordId for full integration
112
133
  const { stonecrop, provideHSTPath, handleHSTChange, formData } = useStonecrop({
113
134
  doctype: taskDoctype,
114
135
  recordId: 'task-123' // or undefined for new records
115
136
  })
116
137
 
117
- // Access the hierarchical state tree
118
- const taskTitle = stonecrop.getStore().get('task.task-123.title')
138
+ // Access the hierarchical state tree directly
139
+ const store = stonecrop.value?.getStore()
140
+ const taskTitle = store?.get('task.task-123.title')
119
141
  </script>
120
142
 
121
143
  <template>
122
144
  <AForm
123
- :schema="formData.schema"
145
+ :schema="taskDoctype.fields"
124
146
  :data="formData"
125
147
  @update="handleHSTChange"
126
148
  />
@@ -276,10 +298,10 @@ export default defineNuxtConfig({
276
298
  ### Plugin Registration
277
299
 
278
300
  The module auto-registers:
279
- - `useStonecrop()` - Main composable for HST integration
280
- - `useTableNavigation()` - Helper for table-to-detail navigation
301
+ - `useStonecropRegistry()` - Composable for wiring data clients and `getMeta` after plugin install
302
+ - `useStonecrop()` - Main composable for HST integration (from `@stonecrop/stonecrop`)
281
303
  - Pinia store configuration
282
- - Component auto-imports (AForm, ATable, etc.)
304
+ - AForm and ATable component registration (from `@stonecrop/aform`)
283
305
 
284
306
  ## Why Schema-Driven?
285
307
 
@@ -299,6 +321,49 @@ The module auto-registers:
299
321
  - **Self-Documenting**: Schemas serve as data model documentation
300
322
  - **Easy Updates**: Change schema, UI updates automatically
301
323
 
324
+ ## `useStonecropRegistry()` — Configuring the Framework After Install
325
+
326
+ The `@stonecrop/nuxt` runtime plugin installs `StonecropPlugin` with a router but no data transport, because data clients (GraphQL, tRPC, REST) are application-defined and cannot be serialised through `nuxt.config.ts` module options.
327
+
328
+ `useStonecropRegistry()` is the documented extension point for wiring your data transport after plugin install. Call it in a client-side plugin:
329
+
330
+ ```typescript
331
+ // app/plugins/stonecrop.client.ts
332
+ export default defineNuxtPlugin(() => {
333
+ const { setMeta, setFetchRecord, setFetchRecords } = useStonecropRegistry()
334
+
335
+ // setMeta — resolves doctype metadata from the current route
336
+ // The context has { path, segments } from the router; adapt to your API.
337
+ setMeta(async ({ segments }) => {
338
+ // Example: segments[0] is the doctype slug, e.g. "task" from /task/123
339
+ const doctype = segments[0]
340
+ const meta = await $fetch(`/api/doctypes/${doctype}`)
341
+ return meta
342
+ })
343
+
344
+ // setFetchRecord — replaces the default REST stub in useStonecrop({ recordId })
345
+ setFetchRecord(async (doctype, id) => {
346
+ return await $fetch(`/api/${doctype.slug}/${id}`)
347
+ })
348
+
349
+ // setFetchRecords — replaces the default REST stub for list loading
350
+ setFetchRecords(async (doctype) => {
351
+ return await $fetch(`/api/${doctype.slug}`)
352
+ })
353
+ })
354
+ ```
355
+
356
+ **Why a composable and not module options?** Nuxt module options are serialised at build time — functions cannot be passed through `nuxt.config.ts`. `useStonecropRegistry()` is the composable equivalent of `usePinia()` or `useNuxtApp()`: a stable, typed API for configuring the framework singleton after it has been installed.
357
+
358
+ ### API
359
+
360
+ | Method | Signature | Description |
361
+ |--------|-----------|-------------|
362
+ | `setMeta` | `(fn: (ctx) => DoctypeMeta \| Promise<DoctypeMeta>) => void` | Sets the `getMeta` function on the Registry. Called by `useStonecrop()` to lazy-load doctype metadata for the current route. `ctx` = `{ path, segments }`. |
363
+ | `setFetchRecord` | `(fn: (doctype, id) => Promise<Record \| null>) => void` | Replaces the default REST fetch stub in `Stonecrop.getRecord()`. Enables GraphQL-backed or any other custom transport. |
364
+ | `setFetchRecords` | `(fn: (doctype) => Promise<Record[]>) => void` | Replaces the default REST fetch stub in `Stonecrop.getRecords()`. |
365
+ | `registry` | `Registry` | The raw Registry instance, for advanced use cases. |
366
+
302
367
  ## Advanced Features
303
368
 
304
369
  ### Hierarchical State Tree (HST)
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
3
  "configKey": "stonecrop",
4
- "version": "0.10.2",
4
+ "version": "0.10.3",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "unknown"
package/dist/module.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { existsSync, realpathSync } from 'node:fs';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { dirname, extname } from 'node:path';
4
- import { createResolver, defineNuxtModule, useLogger, addVitePlugin, addLayout, extendPages, addServerHandler, addPlugin } from '@nuxt/kit';
4
+ import { createResolver, defineNuxtModule, useLogger, addVitePlugin, addLayout, extendPages, addServerHandler, addPlugin, addImportsDir } from '@nuxt/kit';
5
5
 
6
6
  function createSymlinkedPackagesPlugin(options) {
7
7
  const { rootDir, packages, logger } = options;
@@ -96,8 +96,7 @@ const module$1 = defineNuxtModule({
96
96
  });
97
97
  addVitePlugin(symlinkedPackagesPlugin);
98
98
  }
99
- const layoutsDir = resolve("runtime/layouts");
100
- const homepage = resolve(layoutsDir, "StonecropHome.vue");
99
+ const homepage = resolve("runtime/app/layouts/StonecropHome.vue");
101
100
  addLayout(homepage, "home");
102
101
  const appDir = nuxt.options.srcDir;
103
102
  const doctypesDir = resolve(appDir, options.doctypesDir ?? "doctypes");
@@ -186,9 +185,8 @@ const module$1 = defineNuxtModule({
186
185
  }
187
186
  if (options.docbuilder) {
188
187
  logger.log("DocBuilder enabled, adding routes and handlers");
189
- const pagesDir = resolve("runtime/pages");
190
- const docBuilderIndex = resolve(pagesDir, "DocBuilderIndex.vue");
191
- const docBuilderDetail = resolve(pagesDir, "DocBuilderDetail.vue");
188
+ const docBuilderIndex = resolve("runtime/app/pages/DocBuilderIndex.vue");
189
+ const docBuilderDetail = resolve("runtime/app/pages/DocBuilderDetail.vue");
192
190
  extendPages((pages) => {
193
191
  pages.push({
194
192
  name: "docbuilder-index",
@@ -204,24 +202,24 @@ const module$1 = defineNuxtModule({
204
202
  });
205
203
  const handlersDir = resolve("runtime/server/api/docbuilder");
206
204
  addServerHandler({
207
- route: "/api/docbuilder/doctypes",
205
+ route: "/api/_stonecrop/docbuilder/doctypes",
208
206
  handler: resolve(handlersDir, "doctypes.get")
209
207
  });
210
208
  addServerHandler({
211
- route: "/api/docbuilder/:doctype",
209
+ route: "/api/_stonecrop/docbuilder/:doctype",
212
210
  handler: resolve(handlersDir, "[doctype].get")
213
211
  });
214
212
  addServerHandler({
215
- route: "/api/docbuilder/validate",
213
+ route: "/api/_stonecrop/docbuilder/validate",
216
214
  method: "post",
217
215
  handler: resolve(handlersDir, "validate.post")
218
216
  });
219
217
  addServerHandler({
220
- route: "/api/docbuilder/save",
218
+ route: "/api/_stonecrop/docbuilder/save",
221
219
  method: "post",
222
220
  handler: resolve(handlersDir, "save.post")
223
221
  });
224
- logger.log("Added DocBuilder API handlers");
222
+ logger.log("Added DocBuilder API handlers at /api/_stonecrop/docbuilder/");
225
223
  }
226
224
  const pluginPath = resolve("./runtime/plugin");
227
225
  try {
@@ -230,6 +228,7 @@ const module$1 = defineNuxtModule({
230
228
  logger.error("Error adding plugin:", pluginError);
231
229
  throw new Error(`[@stonecrop/nuxt] Failed to add plugin at ${pluginPath}`);
232
230
  }
231
+ addImportsDir(resolve("runtime/app/composables"));
233
232
  }
234
233
  });
235
234
 
@@ -0,0 +1,141 @@
1
+ import type { DataClient, DoctypeMeta } from '@stonecrop/schema';
2
+ import type { RouteContext } from '@stonecrop/stonecrop';
3
+ /**
4
+ * Provides a stable, documented API for accessing and configuring the Stonecrop
5
+ * Registry instance after the `@stonecrop/nuxt` plugin has installed it.
6
+ *
7
+ * ## Why This Composable Exists
8
+ *
9
+ * Stonecrop's architecture separates concerns across packages:
10
+ * - **@stonecrop/schema**: Defines doctype schemas, `DoctypeContext`, and `DataClient` interface
11
+ * - **@stonecrop/stonecrop**: Core framework with `RouteContext` (path + segments) for routing
12
+ * - **@stonecrop/graphql-client**: Reference `DataClient` implementation using GraphQL
13
+ * - **@stonecrop/nuxt**: Nuxt integration that bootstraps the Registry and Stonecrop instances
14
+ *
15
+ * This composable bridges Nuxt's plugin lifecycle with Stonecrop's registry, allowing
16
+ * applications to inject their data client and configure metadata fetching after the
17
+ * framework is mounted.
18
+ *
19
+ * ## RouteContext vs DoctypeContext
20
+ *
21
+ * - **RouteContext** (`{ path, segments }`): Raw URL routing context. Used by the router
22
+ * layer to identify "where we are" in the application (e.g., `/plan/123` → segments `['plan', '123']`).
23
+ *
24
+ * - **DoctypeContext** (`{ doctype, recordId? }`): Semantic doctype context. Used by the
25
+ * data layer to identify "what we're working with" (e.g., `{ doctype: 'Plan', recordId: '123' }`).
26
+ *
27
+ * Your `setMeta` implementation bridges these: extract doctype/recordId from the route
28
+ * segments and pass `DoctypeContext` to your data client.
29
+ *
30
+ * ## Data Flow
31
+ *
32
+ * ```
33
+ * URL Route (/plan/123)
34
+ * ↓
35
+ * RouteContext ({ path: '/plan/123', segments: ['plan', '123'] })
36
+ * ↓
37
+ * getMeta (your implementation)
38
+ * ↓
39
+ * DoctypeContext ({ doctype: 'Plan', recordId: '123' })
40
+ * ↓
41
+ * DataClient.getMeta() → DoctypeMeta
42
+ * ```
43
+ *
44
+ * @example
45
+ * ```ts
46
+ * // app/plugins/stonecrop.client.ts
47
+ * import { StonecropClient } from '@stonecrop/graphql-client'
48
+ *
49
+ * export default defineNuxtPlugin(() => {
50
+ * const client = new StonecropClient({ endpoint: '/graphql' })
51
+ * const { setClient, setMeta } = useStonecropRegistry()
52
+ *
53
+ * // Set the data client for record fetching
54
+ * setClient(client)
55
+ *
56
+ * // Bridge RouteContext → DoctypeContext for metadata fetching
57
+ * setMeta(({ segments }) => {
58
+ * const doctype = segments[0] // e.g. "plan" → doctype "Plan"
59
+ * return client.getMeta({ doctype }) // client expects DoctypeContext
60
+ * })
61
+ * })
62
+ * ```
63
+ *
64
+ * @public
65
+ */
66
+ export declare function useStonecropRegistry(): {
67
+ /**
68
+ * The raw Registry instance, for advanced use cases.
69
+ * Prefer the typed setter methods below for normal configuration.
70
+ */
71
+ registry: {
72
+ getMeta?: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>;
73
+ };
74
+ /**
75
+ * Set the data client on the Stonecrop instance.
76
+ * Required before fetching records or dispatching actions.
77
+ *
78
+ * @param client - DataClient implementation (e.g., StonecropClient from \@stonecrop/graphql-client)
79
+ *
80
+ * @throws Error if Stonecrop instance is not available
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * const client = new StonecropClient({ endpoint: '/graphql' })
85
+ * setClient(client)
86
+ * ```
87
+ */
88
+ setClient(client: DataClient): void;
89
+ /**
90
+ * Get the currently configured data client.
91
+ * @returns The DataClient instance or undefined if not set
92
+ */
93
+ getClient(): DataClient | undefined;
94
+ /**
95
+ * Dispatch an action to the server via the configured data client.
96
+ * All state changes flow through this single mutation endpoint.
97
+ *
98
+ * @param doctype - Doctype reference (name and optional slug)
99
+ * @param action - Action name to execute (e.g., 'SUBMIT', 'APPROVE', 'save')
100
+ * @param args - Action arguments (typically record ID and/or form data)
101
+ * @returns Action result with success status, response data, and any error
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * // Save a record
106
+ * const result = await dispatchAction(doctype, 'save', [{ id: recordId, data: formData }])
107
+ *
108
+ * // Submit for approval
109
+ * const result = await dispatchAction(doctype, 'SUBMIT', [recordId])
110
+ * ```
111
+ */
112
+ dispatchAction(doctype: {
113
+ name: string;
114
+ slug?: string;
115
+ }, action: string, args?: unknown[]): Promise<{
116
+ success: boolean;
117
+ data: unknown;
118
+ error: string | null;
119
+ }>;
120
+ /**
121
+ * Set the `getMeta` function on the Registry.
122
+ * Called by `useStonecrop()` to lazy-load doctype metadata for the current route.
123
+ *
124
+ * You must bridge `RouteContext` → `DoctypeContext`:
125
+ * - Extract doctype name from `segments` (e.g., `segments[0]`)
126
+ * - Extract record ID from `segments` if present (e.g., `segments[1]`)
127
+ * - Pass `DoctypeContext` to your data client
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * // Map route to doctype
132
+ * setMeta(({ segments }) => {
133
+ * const doctype = segments[0] // /plan/123 → 'plan'
134
+ * return client.getMeta({ doctype }) // client expects DoctypeContext
135
+ * })
136
+ * ```
137
+ *
138
+ * @param fn - Function that receives RouteContext and returns DoctypeMeta.
139
+ */
140
+ setMeta(fn: (routeContext: RouteContext) => DoctypeMeta | Promise<DoctypeMeta>): void;
141
+ };
@@ -0,0 +1,98 @@
1
+ import { useNuxtApp } from "nuxt/app";
2
+ export function useStonecropRegistry() {
3
+ const nuxtApp = useNuxtApp();
4
+ const registry = nuxtApp.$registry;
5
+ if (!registry) {
6
+ throw new Error(
7
+ "[useStonecropRegistry] The Stonecrop Registry is not available. Ensure @stonecrop/nuxt is installed and the plugin has run before calling this composable."
8
+ );
9
+ }
10
+ const stonecrop = nuxtApp.$stonecrop;
11
+ return {
12
+ /**
13
+ * The raw Registry instance, for advanced use cases.
14
+ * Prefer the typed setter methods below for normal configuration.
15
+ */
16
+ registry,
17
+ /**
18
+ * Set the data client on the Stonecrop instance.
19
+ * Required before fetching records or dispatching actions.
20
+ *
21
+ * @param client - DataClient implementation (e.g., StonecropClient from \@stonecrop/graphql-client)
22
+ *
23
+ * @throws Error if Stonecrop instance is not available
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const client = new StonecropClient({ endpoint: '/graphql' })
28
+ * setClient(client)
29
+ * ```
30
+ */
31
+ setClient(client) {
32
+ if (!stonecrop) {
33
+ throw new Error(
34
+ "[useStonecropRegistry] Stonecrop instance is not available. Ensure @stonecrop/nuxt is installed and the plugin has run."
35
+ );
36
+ }
37
+ stonecrop.setClient(client);
38
+ },
39
+ /**
40
+ * Get the currently configured data client.
41
+ * @returns The DataClient instance or undefined if not set
42
+ */
43
+ getClient() {
44
+ return stonecrop?.getClient();
45
+ },
46
+ /**
47
+ * Dispatch an action to the server via the configured data client.
48
+ * All state changes flow through this single mutation endpoint.
49
+ *
50
+ * @param doctype - Doctype reference (name and optional slug)
51
+ * @param action - Action name to execute (e.g., 'SUBMIT', 'APPROVE', 'save')
52
+ * @param args - Action arguments (typically record ID and/or form data)
53
+ * @returns Action result with success status, response data, and any error
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * // Save a record
58
+ * const result = await dispatchAction(doctype, 'save', [{ id: recordId, data: formData }])
59
+ *
60
+ * // Submit for approval
61
+ * const result = await dispatchAction(doctype, 'SUBMIT', [recordId])
62
+ * ```
63
+ */
64
+ dispatchAction(doctype, action, args) {
65
+ if (!stonecrop) {
66
+ return Promise.reject(
67
+ new Error(
68
+ "[useStonecropRegistry] Stonecrop instance is not available. Ensure @stonecrop/nuxt is installed and the plugin has run."
69
+ )
70
+ );
71
+ }
72
+ return stonecrop.dispatchAction(doctype, action, args);
73
+ },
74
+ /**
75
+ * Set the `getMeta` function on the Registry.
76
+ * Called by `useStonecrop()` to lazy-load doctype metadata for the current route.
77
+ *
78
+ * You must bridge `RouteContext` → `DoctypeContext`:
79
+ * - Extract doctype name from `segments` (e.g., `segments[0]`)
80
+ * - Extract record ID from `segments` if present (e.g., `segments[1]`)
81
+ * - Pass `DoctypeContext` to your data client
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * // Map route to doctype
86
+ * setMeta(({ segments }) => {
87
+ * const doctype = segments[0] // /plan/123 → 'plan'
88
+ * return client.getMeta({ doctype }) // client expects DoctypeContext
89
+ * })
90
+ * ```
91
+ *
92
+ * @param fn - Function that receives RouteContext and returns DoctypeMeta.
93
+ */
94
+ setMeta(fn) {
95
+ registry.getMeta = fn;
96
+ }
97
+ };
98
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/nuxt",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
4
4
  "description": "Nuxt module for Stonecrop",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -44,14 +44,14 @@
44
44
  "jiti": "^2.4.2",
45
45
  "pathe": "^2.0.3",
46
46
  "prompts": "^2.4.2",
47
- "@stonecrop/aform": "0.10.2",
48
- "@stonecrop/casl-middleware": "0.10.2",
49
- "@stonecrop/graphql-middleware": "0.10.2",
50
- "@stonecrop/nuxt-grafserv": "0.10.2",
51
- "@stonecrop/node-editor": "0.10.2",
52
- "@stonecrop/stonecrop": "0.10.2",
53
- "@stonecrop/schema": "0.10.2",
54
- "@stonecrop/atable": "0.10.2"
47
+ "@stonecrop/aform": "0.10.3",
48
+ "@stonecrop/atable": "0.10.3",
49
+ "@stonecrop/casl-middleware": "0.10.3",
50
+ "@stonecrop/node-editor": "0.10.3",
51
+ "@stonecrop/graphql-middleware": "0.10.3",
52
+ "@stonecrop/nuxt-grafserv": "0.10.3",
53
+ "@stonecrop/schema": "0.10.3",
54
+ "@stonecrop/stonecrop": "0.10.3"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@eslint/js": "^9.39.2",