@stonecrop/stonecrop 0.4.28 → 0.4.29

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.
@@ -1,11 +1,9 @@
1
1
  import { createPinia } from 'pinia';
2
2
  import { PiniaSharedState } from 'pinia-shared-state';
3
- // import { PiniaUndo } from 'pinia-undo'
4
3
  const pinia = createPinia();
5
4
  // Pass the plugin to your application's pinia plugin
6
5
  pinia.use(PiniaSharedState({
7
6
  enable: true,
8
7
  initialize: true,
9
8
  }));
10
- // pinia.use(PiniaUndo)
11
9
  export { pinia };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/stonecrop",
3
- "version": "0.4.28",
3
+ "version": "0.4.29",
4
4
  "description": "schema helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,11 +34,10 @@
34
34
  "immutable": "^5.1.3",
35
35
  "pinia": "^3.0.3",
36
36
  "pinia-shared-state": "^1.0.1",
37
- "pinia-undo": "^0.2.4",
38
- "pinia-xstate": "^2.2.1",
37
+ "pinia-xstate": "^3.0.0",
39
38
  "vue": "^3.5.17",
40
39
  "vue-router": "^4.5.1",
41
- "xstate": "^4.38.3"
40
+ "xstate": "^5.20.1"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@microsoft/api-documenter": "^7.26.29",
@@ -47,13 +46,17 @@
47
46
  "@typescript-eslint/eslint-plugin": "^7.18.0",
48
47
  "@typescript-eslint/parser": "^7.18.0",
49
48
  "@vitejs/plugin-vue": "^5.2.4",
49
+ "@vitest/coverage-istanbul": "^3.2.4",
50
+ "@vue/test-utils": "^2.4.6",
50
51
  "eslint": "^8.57.1",
51
52
  "eslint-config-prettier": "^8.10.0",
52
53
  "eslint-plugin-vue": "^9.33.0",
54
+ "jsdom": "^26.1.0",
53
55
  "typescript": "^5.8.3",
54
56
  "vite": "^6.3.5",
55
- "@stonecrop/aform": "0.4.28",
56
- "@stonecrop/atable": "0.4.28",
57
+ "vitest": "^3.2.4",
58
+ "@stonecrop/atable": "0.4.29",
59
+ "@stonecrop/aform": "0.4.29",
57
60
  "stonecrop-rig": "0.2.22"
58
61
  },
59
62
  "publishConfig": {
@@ -68,6 +71,10 @@
68
71
  "build": "heft build && vite build && rushx docs",
69
72
  "docs": "cd ../common/autoinstallers/doc-tools && node generate-docs.mjs stonecrop",
70
73
  "lint": "eslint . --ext .ts,.vue",
71
- "preview": "vite preview"
74
+ "preview": "vite preview",
75
+ "test": "vitest run --coverage.enabled false",
76
+ "test:watch": "vitest watch",
77
+ "test:coverage": "vitest run --coverage",
78
+ "test:ui": "vitest --ui"
72
79
  }
73
80
  }
package/src/composable.ts CHANGED
@@ -27,6 +27,8 @@ export function useStonecrop(registry?: Registry): StonecropReturn {
27
27
  registry = inject<Registry>('$registry')
28
28
  }
29
29
 
30
+ if (!registry || !registry.router) return
31
+
30
32
  let store: ReturnType<typeof useDataStore>
31
33
  try {
32
34
  store = useDataStore()
@@ -34,11 +36,7 @@ export function useStonecrop(registry?: Registry): StonecropReturn {
34
36
  throw new Error('Please enable the Stonecrop plugin before using the Stonecrop composable')
35
37
  }
36
38
 
37
- // @ts-expect-error TODO: handle empty registry passed to Stonecrop
38
39
  stonecrop.value = new Stonecrop(registry, store)
39
-
40
- if (!registry || !registry.router) return
41
-
42
40
  const route = registry.router.currentRoute.value
43
41
  const doctypeSlug = route.params.records?.toString().toLowerCase()
44
42
  const recordId = route.params.record?.toString().toLowerCase()
@@ -62,7 +60,7 @@ export function useStonecrop(registry?: Registry): StonecropReturn {
62
60
  }
63
61
  }
64
62
 
65
- stonecrop.value.runAction(doctype, 'LOAD', recordId ? [recordId] : undefined)
63
+ stonecrop.value.runAction(doctype, 'load', recordId ? [recordId] : undefined)
66
64
  }
67
65
  })
68
66
 
package/src/doctype.ts CHANGED
@@ -59,8 +59,19 @@ export default class DoctypeMeta {
59
59
  }
60
60
 
61
61
  /**
62
- * Converts the registered doctype to a slug (kebab-case)
62
+ * Converts the registered doctype string to a slug (kebab-case). The following conversions are made:
63
+ * - It replaces camelCase and PascalCase with kebab-case strings
64
+ * - It replaces spaces and underscores with hyphens
65
+ * - It converts the string to lowercase
66
+ *
63
67
  * @returns The slugified doctype string
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const doctype = new DoctypeMeta('TaskItem', schema, workflow, actions
72
+ * console.log(doctype.slug) // 'task-item'
73
+ * ```
74
+ *
64
75
  * @public
65
76
  */
66
77
  get slug() {
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ import { type StonecropReturn, useStonecrop } from './composable'
16
16
  import DoctypeMeta from './doctype'
17
17
  import Registry from './registry'
18
18
  import Stonecrop from './plugins'
19
+ import { Stonecrop as StonecropClass } from './stonecrop'
19
20
  export type { ImmutableDoctype, MutableDoctype, Schema, InstallOptions } from './types'
20
21
 
21
- export { DoctypeMeta, Registry, Stonecrop, StonecropReturn, useStonecrop }
22
+ export { DoctypeMeta, Registry, Stonecrop, StonecropClass, StonecropReturn, useStonecrop }
package/src/registry.ts CHANGED
@@ -17,25 +17,19 @@ export default class Registry {
17
17
  *
18
18
  * @defaultValue 'Registry'
19
19
  */
20
- name: string
20
+ readonly name: string
21
21
 
22
22
  /**
23
23
  * The registry property contains a collection of doctypes
24
24
  * @see {@link DoctypeMeta}
25
25
  */
26
- registry: Record<string, DoctypeMeta>
26
+ readonly registry: Record<string, DoctypeMeta>
27
27
 
28
28
  /**
29
29
  * The Vue router instance
30
30
  * @see {@link https://router.vuejs.org/}
31
31
  */
32
- router?: Router
33
-
34
- /**
35
- * The getMeta function fetches doctype metadata from an API
36
- * @see {@link DoctypeMeta}
37
- */
38
- getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>
32
+ readonly router?: Router
39
33
 
40
34
  constructor(router?: Router, getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>) {
41
35
  if (Registry._root) {
@@ -48,6 +42,12 @@ export default class Registry {
48
42
  this.getMeta = getMeta
49
43
  }
50
44
 
45
+ /**
46
+ * The getMeta function fetches doctype metadata from an API
47
+ * @see {@link DoctypeMeta}
48
+ */
49
+ getMeta?: (doctype: string) => DoctypeMeta | Promise<DoctypeMeta>
50
+
51
51
  /**
52
52
  * Get doctype metadata
53
53
  * @param doctype - The doctype to fetch metadata for
@@ -67,4 +67,17 @@ export default class Registry {
67
67
  })
68
68
  }
69
69
  }
70
+
71
+ // TODO: should we allow clearing the registry at all?
72
+ // clear() {
73
+ // this.registry = {}
74
+ // if (this.router) {
75
+ // const routes = this.router.getRoutes()
76
+ // for (const route of routes) {
77
+ // if (route.name) {
78
+ // this.router.removeRoute(route.name)
79
+ // }
80
+ // }
81
+ // }
82
+ // }
70
83
  }
package/src/stonecrop.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { createActor, createMachine } from 'xstate'
2
+
1
3
  import DoctypeMeta from './doctype'
2
4
  import { NotImplementedError } from './exceptions'
3
5
  import Registry from './registry'
4
6
  import { useDataStore } from './stores/data'
5
- import type { ImmutableDoctype, Schema } from './types'
6
7
 
7
8
  /**
8
9
  * Stonecrop class
@@ -48,42 +49,10 @@ export class Stonecrop {
48
49
  */
49
50
  store: ReturnType<typeof useDataStore>
50
51
 
51
- /**
52
- * schema - The Stonecrop schema; the schema is a subset of the registry
53
- * @example
54
- * ```ts
55
- * {
56
- * doctype: 'Task',
57
- * schema: {
58
- * title: 'string',
59
- * description: 'string',
60
- * ...
61
- * }
62
- * }
63
- * ```
64
- * @see {@link Registry}
65
- * @see {@link DoctypeMeta}
66
- * @see {@link DoctypeMeta.schema}
67
- */
68
- schema?: Schema
69
-
70
- /**
71
- * The workflow is a subset of the registry
72
- */
73
- workflow?: ImmutableDoctype['workflow']
74
-
75
- /**
76
- * The actions are a subset of the registry
77
- */
78
- actions?: ImmutableDoctype['actions']
79
-
80
52
  /**
81
53
  * @param registry - The immutable registry
82
54
  * @param store - The mutable Pinia store
83
- * @param schema - The Stonecrop schema
84
- * @param workflow - The Stonecrop workflow
85
- * @param actions - The Stonecrop actions
86
- * @returns The Stonecrop instance with the given registry, store, schema, workflow, and actions. If a Stonecrop instance has already been created, it returns the existing instance instead of creating a new one.
55
+ * @returns The Stonecrop instance with the given registry and store. If a Stonecrop instance has already been created, it returns the existing instance instead of creating a new one.
87
56
  * @example
88
57
  * ```ts
89
58
  * const registry = new Registry()
@@ -91,22 +60,13 @@ export class Stonecrop {
91
60
  * const stonecrop = new Stonecrop(registry, store)
92
61
  * ```
93
62
  */
94
- constructor(
95
- registry: Registry,
96
- store: ReturnType<typeof useDataStore>,
97
- schema?: Schema,
98
- workflow?: ImmutableDoctype['workflow'],
99
- actions?: ImmutableDoctype['actions']
100
- ) {
63
+ constructor(registry: Registry, store: ReturnType<typeof useDataStore>) {
101
64
  if (Stonecrop._root) {
102
65
  return Stonecrop._root
103
66
  }
104
67
  Stonecrop._root = this
105
68
  this.registry = registry
106
69
  this.store = store
107
- this.schema = schema // new Registry(schema)
108
- this.workflow = workflow
109
- this.actions = actions
110
70
  }
111
71
 
112
72
  /**
@@ -119,16 +79,14 @@ export class Stonecrop {
119
79
  * ```
120
80
  */
121
81
  setup(doctype: DoctypeMeta): void {
122
- void this.getMeta(doctype)
123
- this.getWorkflow(doctype)
124
- this.getActions(doctype)
82
+ void this.getMeta(doctype.doctype)
125
83
  }
126
84
 
127
85
  /**
128
86
  * Gets the meta for the given doctype
129
87
  * @param doctype - The doctype to get meta for
130
88
  * @returns The meta for the given doctype
131
- * @throws NotImplementedError
89
+ * @throws `NotImplementedError` if the `getMeta` function is not implemented for the doctype in the registry
132
90
  * @example
133
91
  * ```ts
134
92
  * const doctype = await registry.getMeta('Task')
@@ -136,36 +94,11 @@ export class Stonecrop {
136
94
  * ```
137
95
  * @see {@link DoctypeMeta}
138
96
  */
139
- getMeta(doctype: DoctypeMeta): DoctypeMeta | Promise<DoctypeMeta> | never {
140
- return this.registry.getMeta ? this.registry.getMeta(doctype.doctype) : new NotImplementedError(doctype.doctype)
141
- }
142
-
143
- /**
144
- * Gets the workflow for the given doctype
145
- * @param doctype - The doctype to get workflow for
146
- * @example
147
- * ```ts
148
- * const doctype = await registry.getMeta('Task')
149
- * stonecrop.getWorkflow(doctype)
150
- * ```
151
- */
152
- getWorkflow(doctype: DoctypeMeta): void {
153
- const doctypeRegistry = this.registry.registry[doctype.slug]
154
- this.workflow = doctypeRegistry.workflow
155
- }
156
-
157
- /**
158
- * Gets the actions for the given doctype
159
- * @param doctype - The doctype to get actions for
160
- * @example
161
- * ```ts
162
- * const doctype = await registry.getMeta('Task')
163
- * stonecrop.getActions(doctype)
164
- * ```
165
- */
166
- getActions(doctype: DoctypeMeta): void {
167
- const doctypeRegistry = this.registry.registry[doctype.slug]
168
- this.actions = doctypeRegistry.actions
97
+ async getMeta(doctype: string): Promise<DoctypeMeta> | never {
98
+ if (!this.registry.getMeta) {
99
+ throw new NotImplementedError(`getMeta function is not implemented for ${doctype} in the registry`)
100
+ }
101
+ return await this.registry.getMeta(doctype)
169
102
  }
170
103
 
171
104
  /**
@@ -216,32 +149,37 @@ export class Stonecrop {
216
149
  * @example
217
150
  * ```ts
218
151
  * const doctype = await registry.getMeta('Task')
219
- * stonecrop.runAction(doctype, 'CREATE')
152
+ * stonecrop.runAction(doctype, 'create')
220
153
  * ```
221
154
  * @example
222
155
  * ```ts
223
156
  * const doctype = await registry.getMeta('Task')
224
- * stonecrop.runAction(doctype, 'UPDATE', ['TASK-00001'])
157
+ * stonecrop.runAction(doctype, 'update', ['TASK-00001'])
225
158
  * ```
226
159
  * @example
227
160
  * ```ts
228
161
  * const doctype = await registry.getMeta('Task')
229
- * stonecrop.runAction(doctype, 'DELETE', ['TASK-00001'])
162
+ * stonecrop.runAction(doctype, 'delete', ['TASK-00001'])
230
163
  * ```
231
164
  * @example
232
165
  * ```ts
233
166
  * const doctype = await registry.getMeta('Task')
234
- * stonecrop.runAction(doctype, 'TRANSITION', ['TASK-00001', 'TASK-00002'])
167
+ * stonecrop.runAction(doctype, 'merge', ['TASK-00001', 'TASK-00002'])
235
168
  * ```
236
169
  */
237
170
  runAction(doctype: DoctypeMeta, action: string, id?: string[]): void {
238
- const doctypeRegistry = this.registry.registry[doctype.slug]
239
- const actions = doctypeRegistry.actions?.get(action)
171
+ const registry = this.registry.registry[doctype.slug]
172
+ const actions = registry.actions?.get(action)
173
+ const workflow = registry.workflow
240
174
 
241
175
  // trigger the action on the state machine
242
- if (this.workflow) {
243
- const { initialState } = this.workflow
244
- this.workflow.transition(initialState, { type: action })
176
+ if (workflow) {
177
+ const machine = createMachine(workflow)
178
+ const actor = createActor(machine)
179
+
180
+ // TODO: this shouldn't spawn an actor at the initial state always; look into persistence
181
+ actor.start()
182
+ actor.send({ type: action, id })
245
183
 
246
184
  // run actions after state machine transition
247
185
  // TODO: should this happen with or without the workflow?
@@ -1,6 +1,5 @@
1
1
  import { createPinia } from 'pinia'
2
2
  import { PiniaSharedState } from 'pinia-shared-state'
3
- // import { PiniaUndo } from 'pinia-undo'
4
3
 
5
4
  const pinia = createPinia()
6
5
 
@@ -11,6 +10,5 @@ pinia.use(
11
10
  initialize: true,
12
11
  })
13
12
  )
14
- // pinia.use(PiniaUndo)
15
13
 
16
14
  export { pinia }
@@ -2,7 +2,7 @@ import type { SchemaTypes } from '@stonecrop/aform'
2
2
  import { List, Map } from 'immutable'
3
3
  import type { Component } from 'vue'
4
4
  import type { Router } from 'vue-router'
5
- import type { MachineConfig, StateMachine } from 'xstate'
5
+ import type { AnyStateNodeConfig, UnknownMachineConfig } from 'xstate'
6
6
 
7
7
  import DoctypeMeta from '../doctype'
8
8
 
@@ -13,7 +13,7 @@ import DoctypeMeta from '../doctype'
13
13
  export type ImmutableDoctype = {
14
14
  // TODO: allow schema to be a function
15
15
  readonly schema?: List<SchemaTypes>
16
- readonly workflow: StateMachine<unknown, any, any>
16
+ readonly workflow?: UnknownMachineConfig | AnyStateNodeConfig
17
17
  readonly actions?: Map<string, string[]>
18
18
  }
19
19
 
@@ -24,7 +24,7 @@ export type ImmutableDoctype = {
24
24
  export type MutableDoctype = {
25
25
  // TODO: allow schema to be a function
26
26
  schema?: SchemaTypes[]
27
- workflow: MachineConfig<unknown, any, any>
27
+ workflow?: UnknownMachineConfig | AnyStateNodeConfig
28
28
  actions?: Record<string, string[]>
29
29
  }
30
30
 
@@ -1,31 +0,0 @@
1
- export declare const counterMachine: import("xstate").StateMachine<{
2
- count: number;
3
- }, any, import("xstate").AnyEventObject, {
4
- value: any;
5
- context: {
6
- count: number;
7
- };
8
- }, import("xstate").BaseActionObject, import("xstate").ServiceMap, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, import("xstate").AnyEventObject, import("xstate").BaseActionObject, import("xstate").ServiceMap>>;
9
- export declare const useCounterStore: import("pinia").StoreDefinition<string, Pick<import("pinia-xstate").Store<import("xstate").StateMachine<{
10
- count: number;
11
- }, any, import("xstate").AnyEventObject, {
12
- value: any;
13
- context: {
14
- count: number;
15
- };
16
- }, import("xstate").BaseActionObject, import("xstate").ServiceMap, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, import("xstate").AnyEventObject, import("xstate").BaseActionObject, import("xstate").ServiceMap>>>, "state" | "actor">, Pick<import("pinia-xstate").Store<import("xstate").StateMachine<{
17
- count: number;
18
- }, any, import("xstate").AnyEventObject, {
19
- value: any;
20
- context: {
21
- count: number;
22
- };
23
- }, import("xstate").BaseActionObject, import("xstate").ServiceMap, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, import("xstate").AnyEventObject, import("xstate").BaseActionObject, import("xstate").ServiceMap>>>, "state">, Pick<import("pinia-xstate").Store<import("xstate").StateMachine<{
24
- count: number;
25
- }, any, import("xstate").AnyEventObject, {
26
- value: any;
27
- context: {
28
- count: number;
29
- };
30
- }, import("xstate").BaseActionObject, import("xstate").ServiceMap, import("xstate").ResolveTypegenMeta<import("xstate").TypegenDisabled, import("xstate").AnyEventObject, import("xstate").BaseActionObject, import("xstate").ServiceMap>>>, "state" | "send">>;
31
- //# sourceMappingURL=xstate.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"xstate.d.ts","sourceRoot":"","sources":["../../../src/stores/xstate.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,cAAc;;;;;;;0OA0B1B,CAAA;AAGD,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;+PAAyD,CAAA"}
@@ -1,29 +0,0 @@
1
- import { defineStore } from 'pinia';
2
- import { xstate } from 'pinia-xstate';
3
- import { createMachine } from 'xstate';
4
- export const counterMachine = createMachine({
5
- id: 'counter',
6
- initial: 'active',
7
- context: {
8
- count: 0,
9
- },
10
- states: {
11
- active: {
12
- on: {
13
- INC: { actions: 'increment' },
14
- DEC: { actions: 'decrement' },
15
- },
16
- },
17
- },
18
- }, {
19
- actions: {
20
- increment: context => {
21
- context.count = context.count + 1;
22
- },
23
- decrement: context => {
24
- context.count = context.count - 1;
25
- },
26
- },
27
- });
28
- // create a store using the xstate middleware
29
- export const useCounterStore = defineStore(counterMachine.id, xstate(counterMachine));
@@ -1,34 +0,0 @@
1
- import { defineStore } from 'pinia'
2
- import { xstate } from 'pinia-xstate'
3
- import { createMachine } from 'xstate'
4
-
5
- export const counterMachine = createMachine(
6
- {
7
- id: 'counter',
8
- initial: 'active',
9
- context: {
10
- count: 0,
11
- },
12
- states: {
13
- active: {
14
- on: {
15
- INC: { actions: 'increment' },
16
- DEC: { actions: 'decrement' },
17
- },
18
- },
19
- },
20
- },
21
- {
22
- actions: {
23
- increment: context => {
24
- context.count = context.count + 1
25
- },
26
- decrement: context => {
27
- context.count = context.count - 1
28
- },
29
- },
30
- }
31
- )
32
-
33
- // create a store using the xstate middleware
34
- export const useCounterStore = defineStore(counterMachine.id, xstate(counterMachine))