@kellanjs/actioncraft 0.1.0 → 0.2.0

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.
Files changed (87) hide show
  1. package/README.md +411 -302
  2. package/dist/actioncraft-error.d.ts +23 -0
  3. package/dist/actioncraft-error.js +60 -0
  4. package/dist/actioncraft-error.js.map +1 -0
  5. package/dist/actioncraft-prev.d.ts +93 -0
  6. package/dist/actioncraft-prev.js +387 -0
  7. package/dist/actioncraft-prev.js.map +1 -0
  8. package/dist/actioncraft.d.ts +94 -44
  9. package/dist/actioncraft.js +281 -55
  10. package/dist/actioncraft.js.map +1 -1
  11. package/dist/api.d.ts +49 -0
  12. package/dist/api.js +84 -0
  13. package/dist/api.js.map +1 -0
  14. package/dist/classes/action-builder.d.ts +59 -0
  15. package/dist/classes/action-builder.js +95 -0
  16. package/dist/classes/action-builder.js.map +1 -0
  17. package/dist/classes/craft-builder.d.ts +66 -0
  18. package/dist/classes/craft-builder.js +129 -0
  19. package/dist/classes/craft-builder.js.map +1 -0
  20. package/dist/classes/crafter.d.ts +66 -0
  21. package/dist/classes/crafter.js +129 -0
  22. package/dist/classes/crafter.js.map +1 -0
  23. package/dist/classes/error.d.ts +23 -0
  24. package/dist/classes/error.js +60 -0
  25. package/dist/classes/error.js.map +1 -0
  26. package/dist/classes/executor/callbacks.d.ts +6 -0
  27. package/dist/classes/executor/callbacks.js +20 -0
  28. package/dist/classes/executor/callbacks.js.map +1 -0
  29. package/dist/classes/executor/errors.d.ts +29 -0
  30. package/dist/classes/executor/errors.js +114 -0
  31. package/dist/classes/executor/errors.js.map +1 -0
  32. package/dist/classes/executor/executor.d.ts +68 -0
  33. package/dist/classes/executor/executor.js +391 -0
  34. package/dist/classes/executor/executor.js.map +1 -0
  35. package/dist/classes/executor/logging.d.ts +2 -0
  36. package/dist/classes/executor/logging.js +8 -0
  37. package/dist/classes/executor/logging.js.map +1 -0
  38. package/dist/classes/executor/transformation.d.ts +17 -0
  39. package/dist/classes/executor/transformation.js +43 -0
  40. package/dist/classes/executor/transformation.js.map +1 -0
  41. package/dist/classes/executor/validation.d.ts +16 -0
  42. package/dist/classes/executor/validation.js +70 -0
  43. package/dist/classes/executor/validation.js.map +1 -0
  44. package/dist/classes/executor.d.ts +64 -0
  45. package/dist/classes/executor.js +354 -0
  46. package/dist/classes/executor.js.map +1 -0
  47. package/dist/classes/internal.d.ts +10 -0
  48. package/dist/classes/internal.js +5 -0
  49. package/dist/classes/internal.js.map +1 -0
  50. package/dist/core/errors.d.ts +2 -2
  51. package/dist/core/errors.js +5 -5
  52. package/dist/core/errors.js.map +1 -1
  53. package/dist/core/logging.d.ts +1 -1
  54. package/dist/core/transformation.d.ts +2 -2
  55. package/dist/core/validation.d.ts +4 -4
  56. package/dist/core/validation.js +14 -14
  57. package/dist/core/validation.js.map +1 -1
  58. package/dist/craft.d.ts +29 -0
  59. package/dist/craft.js +62 -0
  60. package/dist/craft.js.map +1 -0
  61. package/dist/error.d.ts +21 -6
  62. package/dist/error.js +59 -10
  63. package/dist/error.js.map +1 -1
  64. package/dist/index.d.ts +4 -3
  65. package/dist/index.js +4 -3
  66. package/dist/index.js.map +1 -1
  67. package/dist/initial.d.ts +14 -0
  68. package/dist/initial.js +47 -0
  69. package/dist/initial.js.map +1 -0
  70. package/dist/types/actions.d.ts +67 -25
  71. package/dist/types/builder.d.ts +92 -0
  72. package/dist/types/builder.js +2 -0
  73. package/dist/types/builder.js.map +1 -0
  74. package/dist/types/crafter.d.ts +87 -0
  75. package/dist/types/crafter.js +2 -0
  76. package/dist/types/crafter.js.map +1 -0
  77. package/dist/types/errors.d.ts +25 -17
  78. package/dist/types/inference.d.ts +41 -8
  79. package/dist/types/result.d.ts +8 -14
  80. package/dist/types/result.js +36 -4
  81. package/dist/types/result.js.map +1 -1
  82. package/dist/types/schemas.d.ts +7 -7
  83. package/dist/types/shared.d.ts +14 -6
  84. package/dist/utils.d.ts +30 -6
  85. package/dist/utils.js +68 -8
  86. package/dist/utils.js.map +1 -1
  87. package/package.json +3 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ⚠️🚧 The library hasn't reached a stable release yet. Expect bugs and potentially breaking API changes until then.
2
2
 
3
- # ActionCraft
3
+ # Actioncraft
4
4
 
5
5
  Streamline your server actions.
6
6
 
@@ -21,24 +21,25 @@ Streamline your server actions.
21
21
  - [Example](#example)
22
22
  - [Result Format](#result-format)
23
23
  - [Walkthrough](#walkthrough)
24
- - [.create() - Configure Your Action](#create)
24
+ - [.config() - Configure Your Action](#config)
25
25
  - [.schemas() - Add Validation](#schemas)
26
26
  - [.errors() - Define Custom Errors](#errors)
27
- - [.action() - Implement Business Logic](#action)
27
+ - [.handler() - Implement Business Logic](#handler)
28
28
  - [.callbacks() - Add Lifecycle Hooks](#callbacks)
29
- - [.craft() - Build Your Action](#craft)
30
29
  - [Using Your Actions](#using-your-actions)
31
30
  - [Basic Usage](#basic-usage)
32
31
  - [Error Handling](#error-handling)
33
32
  - [React Forms with useActionState](#react-forms-with-useactionstate)
34
33
  - [Progressive Enhancement](#progressive-enhancement)
35
34
  - [Complete Example](#complete-example)
36
- - [Integrations](#integrations)
37
- - [Utilities](#utilities)
38
- - [React Query](#react-query)
39
35
  - [Advanced Features](#advanced-features)
40
36
  - [Bind Arguments](#bind-arguments)
41
- - [Type Inference Utilities](#type-inference-utilities)
37
+ - [Utilities](#utilities)
38
+ - [Type Inference](#type-inference)
39
+ - [Input Validation](#input-validation)
40
+ - [Integration Utilities](#integration-utilities)
41
+ - [Actioncraft Errors](#actioncraft-errors)
42
+ - [React Query](#react-query)
42
43
 
43
44
  ## Quick Start
44
45
 
@@ -50,18 +51,40 @@ npm install @kellanjs/actioncraft
50
51
 
51
52
  ### Overview
52
53
 
53
- ActionCraft makes it easy to create type-safe server actions with first-class error-handling support. Here's the basic structure you'll follow:
54
+ Actioncraft makes it easy to create type-safe server actions with first-class error-handling support.
55
+
56
+ The library supports two different syntax patterns, aptly referred to as the `action() api` and the `craft() api`. Both are functionally the same, so use whichever you prefer!
57
+
58
+ For the sake of simplicity, this document will use the same pattern (the `action() api`) for all usage examples, but either pattern would produce the exact same result.
59
+
60
+ #### action() api
54
61
 
55
62
  ```typescript
56
- const action = create(...)
63
+ export const example = action() // We call action() first to create a builder to use
64
+ .config(...)
57
65
  .schemas(...)
58
66
  .errors(...)
59
- .action(...)
67
+ .handler(...)
60
68
  .callbacks(...)
61
- .craft();
69
+ .craft(); // And we call craft() last to build and return your type-safe server action
62
70
  ```
63
71
 
64
- ActionCraft uses a fluent builder pattern, making it simple to chain one method after the next to create a full-featured server action. The order in which these methods are defined is important for type inference to work properly, so you'll see this same structure repeated often throughout the documentation. Always make sure to chain your methods together like this for the best experience!
72
+ #### craft() api
73
+
74
+ ```typescript
75
+ // We call craft() first, and it provides us with a builder to use
76
+ export const example = craft(async (action) =>
77
+ action
78
+ .config(...)
79
+ .schemas(...)
80
+ .errors(...)
81
+ .handler(...)
82
+ .callbacks(...)
83
+ // No craft() needed here, because it's already wrapping everything!
84
+ );
85
+ ```
86
+
87
+ Actioncraft uses a fluent builder design, making it simple to chain one method after the next to create a full-featured server action. Regardless of which syntax pattern you use, the order in which the methods are defined is important for type inference to work properly, so you'll see the same structure repeated often throughout the documentation. Always make sure to chain your methods together like this for the best experience!
65
88
 
66
89
  ### Example
67
90
 
@@ -70,7 +93,7 @@ With this basic structure in mind, let's see what a more detailed example looks
70
93
  ```typescript
71
94
  "use server";
72
95
 
73
- import { create } from "@kellanjs/actioncraft";
96
+ import { action } from "@kellanjs/actioncraft";
74
97
  import { z } from "zod";
75
98
 
76
99
  const newUserInputSchema = z.object({
@@ -79,7 +102,11 @@ const newUserInputSchema = z.object({
79
102
  age: z.number(),
80
103
  });
81
104
 
82
- export const createNewUser = create()
105
+ export const createNewUser = action()
106
+ // Define configuration settings
107
+ .config({
108
+ validationErrorFormat: "nested",
109
+ })
83
110
  // Define the validation schema
84
111
  .schemas({
85
112
  inputSchema: newUserInputSchema,
@@ -99,7 +126,7 @@ export const createNewUser = create()
99
126
  }) as const,
100
127
  })
101
128
  // Define your server action logic
102
- .action(async ({ input, errors }) => {
129
+ .handler(async ({ input, errors }) => {
103
130
  // These are your validated input values
104
131
  const { name, email, age } = input;
105
132
 
@@ -112,13 +139,12 @@ export const createNewUser = create()
112
139
 
113
140
  return { newUser };
114
141
  })
115
- // Define lifecycle callbacks (optional)
142
+ // Define lifecycle callbacks
116
143
  .callbacks({
117
- onSettled: (result) => {
144
+ onSettled: ({ result }) => {
118
145
  // Log what happened if you want
119
146
  },
120
147
  })
121
- // Finally, build the full type-safe action
122
148
  .craft();
123
149
  ```
124
150
 
@@ -126,7 +152,7 @@ export const createNewUser = create()
126
152
 
127
153
  Server actions work best when you're returning serializable data. Throwing errors is less effective in this context, because Next.js will sanitize Error class objects that are thrown in your action, leaving you without useful error information on the client. You might see something in development, but in production, if you try to display `error.message`, you'll likely see something along the lines of: "An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details."
128
154
 
129
- ActionCraft was designed with this fundamental behavior in mind! Instead of throwing errors, we're working exclusively with structured, serializable objects every step of the way, so errors in your action will always return the data you need.
155
+ Actioncraft was designed with this fundamental behavior in mind! Instead of throwing errors, we're working exclusively with structured, serializable objects every step of the way, so errors in your action will always return the data you need.
130
156
 
131
157
  The default result format should feel pretty familiar: `{ success: true, data: T } | { success: false, error: E }`
132
158
 
@@ -141,7 +167,7 @@ const handleCreateNewUser = async (userData) => {
141
167
  // If the action was successful, then you get back typed return data
142
168
  toast.success("User created:", result.data.newUser);
143
169
  } else {
144
- // If the action was unsuccessful, then you get type-safe error handling
170
+ // If the action was unsuccessful, then you get fully typed error handling
145
171
  switch (result.error.type) {
146
172
  case "INPUT_VALIDATION":
147
173
  handleInputValidationErrorLogic();
@@ -162,71 +188,81 @@ const handleCreateNewUser = async (userData) => {
162
188
 
163
189
  ## Walkthrough
164
190
 
165
- Now that we've covered the basic structure of an action and looked at a simple example, let's take a more detailed look at each method in the chain and what it can do.
191
+ Now that we've covered the basic structure of an action and looked at a simple example, let's take a more detailed look at how Actioncraft works and what you can do with it.
166
192
 
167
- ### .create()
193
+ ### .config()
168
194
 
169
- Actions always begin with the `create` method. You can use it with no arguments for sensible defaults, or pass a configuration object to customize behavior:
195
+ Actioncraft provides several configuration options to customize your action. Sensible defaults are provided, so you only need to define the `config` if you specifically want to override something. When you want to customize a certain behavior, just pass a configuration object:
170
196
 
171
197
  ```typescript
172
- // We're just taking the default configuration here
173
- const action = create()
198
+ export const getUser = action()
199
+ .config({
200
+ name: "getUser",
201
+ useActionState: true,
202
+ resultFormat: "functional",
203
+ validationErrorFormat: "nested",
204
+ handleThrownError: (error) => ({
205
+ type: "CUSTOM_ERROR",
206
+ message: error.message,
207
+ }) as const,
208
+ })
174
209
  .schemas(...)
175
210
  .errors(...)
176
- .action(...)
211
+ .handler(...)
177
212
  .callbacks(...)
178
213
  .craft();
179
214
  ```
180
215
 
181
- #### Configuration Options
216
+ #### `name: string`
182
217
 
183
- When you want to customize behavior, pass a configuration object:
218
+ **Default:** `undefined`
219
+
220
+ An optional identifier for your action that will be included in error messages to help with debugging:
184
221
 
185
222
  ```typescript
186
- const action = create({
187
- useActionState: true,
188
- resultFormat: "api",
189
- validationErrorFormat: "flattened",
190
- handleThrownError: (error) => ({
191
- type: "CUSTOM_ERROR",
192
- message: error.message,
193
- }) as const,
223
+ export const updateUserProfile = action()
224
+ .config({ name: "updateUserProfile" })
225
+ .schemas({ inputSchema: userSchema })
226
+ .handler(async ({ input }) => {
227
+ // Your handler logic
194
228
  })
195
- .schemas(...)
196
- .errors(...)
197
- .action(...)
198
- .callbacks(...)
199
229
  .craft();
230
+
231
+ // If validation fails, the error message will be:
232
+ // "Input validation failed in action \"updateUserProfile\""
233
+ // instead of just:
234
+ // "Input validation failed"
200
235
  ```
201
236
 
202
- ##### `useActionState: boolean`
237
+ #### `useActionState: boolean`
203
238
 
204
239
  **Default:** `false`
205
240
 
206
241
  Set to `true` to make your action compatible with React's `useActionState` hook:
207
242
 
208
243
  ```typescript
209
- const action = create({ useActionState: true })
244
+ export const getUser = action()
245
+ .config({ useActionState: true })
210
246
  .schemas(...)
211
247
  .errors(...)
212
- .action(...)
248
+ .handler(...)
213
249
  .callbacks(...)
214
250
  .craft();
215
251
 
216
- // Now you can use it with useActionState like this:
217
- const [state, formAction] = useActionState(action, initial(action));
252
+ // Now you can use it with useActionState in your client components like this:
253
+ const [state, action] = useActionState(getUser, initial(getUser));
218
254
  ```
219
255
 
220
- ##### `resultFormat: "api" | "functional"`
256
+ #### `resultFormat: "api" | "functional"`
221
257
 
222
258
  **Default:** `"api"`
223
259
 
224
- ActionCraft supports two different return formats:
260
+ Actioncraft supports two different return formats:
225
261
 
226
262
  - **`"api"`**: `{ success: true, data: T } | { success: false, error: E }`
227
263
  - **`"functional"`**: `{ type: "ok", value: T } | { type: "err", error: E }`
228
264
 
229
- ##### `validationErrorFormat: "flattened" | "nested"`
265
+ #### `validationErrorFormat: "flattened" | "nested"`
230
266
 
231
267
  **Default:** `"flattened"`
232
268
 
@@ -235,19 +271,26 @@ Controls how validation errors are structured:
235
271
  - **`"flattened"`**: Returns a flat array of error messages
236
272
  - **`"nested"`**: Returns a nested object matching your schema structure
237
273
 
238
- ##### `handleThrownError: (error: unknown) => UserDefinedError`
274
+ #### `handleThrownError: (error: unknown) => UserDefinedError`
239
275
 
240
- By default, ActionCraft catches thrown errors and returns a structured error with type `"UNHANDLED"`. You can customize this behavior by passing an error handler function of your own:
276
+ By default, Actioncraft catches thrown errors and returns a structured error with type `"UNHANDLED"`. You can customize this behavior by passing an error handler function of your own:
241
277
 
242
278
  ```typescript
243
- const action = create({
244
- handleThrownError: (error) =>
245
- ({
246
- type: "CUSTOM_ERROR",
247
- message: error instanceof Error ? error.message : "Something went wrong",
248
- timestamp: new Date().toISOString(),
249
- }) as const,
250
- });
279
+ export const getUser = action()
280
+ .config({
281
+ handleThrownError: (error) =>
282
+ ({
283
+ type: "CUSTOM_ERROR",
284
+ message:
285
+ error instanceof Error ? error.message : "Something went wrong",
286
+ timestamp: new Date().toISOString(),
287
+ }) as const,
288
+ })
289
+ .schemas(...)
290
+ .errors(...)
291
+ .handler(...)
292
+ .callbacks(...)
293
+ .craft();
251
294
  ```
252
295
 
253
296
  You can even implement more complex logic if you want:
@@ -296,7 +339,7 @@ handleThrownError: (error: unknown) => {
296
339
  };
297
340
  ```
298
341
 
299
- ActionCraft's types are smart enough to infer all of these possibilities back on the client:
342
+ Actioncraft's types are smart enough to infer all of these possibilities back on the client:
300
343
 
301
344
  ```typescript
302
345
  if (!result.success) {
@@ -309,17 +352,18 @@ Pretty cool!
309
352
 
310
353
  ### .schemas()
311
354
 
312
- With our action configured, let's add validation using schemas. ActionCraft supports any library that implements the **Standard Schema V1** interface. Validation is handled automatically - you just need to provide the schemas:
355
+ With our action configured, let's add validation using schemas. Actioncraft supports any library that implements the **Standard Schema V1** interface. Validation is handled automatically - you just need to provide the schemas:
313
356
 
314
357
  ```typescript
315
- const action = create()
358
+ export const getUser = action()
359
+ .config(...)
316
360
  .schemas({
317
361
  inputSchema,
318
362
  outputSchema,
319
363
  bindSchemas,
320
364
  })
321
365
  .errors(...)
322
- .action(...)
366
+ .handler(...)
323
367
  .callbacks(...)
324
368
  .craft();
325
369
  ```
@@ -340,10 +384,11 @@ Validates arguments bound to the action with `.bind()`. If validation fails, a "
340
384
 
341
385
  ### .errors()
342
386
 
343
- Now that we have validation set up, let's define custom errors that our action can return. ActionCraft makes error handling really easy by letting you define structured error types:
387
+ Now that we have validation set up, let's define custom errors that our action can return. Actioncraft makes error handling really easy by letting you define structured error types:
344
388
 
345
389
  ```typescript
346
- const action = create()
390
+ export const errorExamples = action()
391
+ .config(...)
347
392
  .schemas(...)
348
393
  .errors({
349
394
  unauthorized: () =>
@@ -364,7 +409,7 @@ const action = create()
364
409
  email,
365
410
  }) as const,
366
411
  })
367
- .action(...)
412
+ .handler(...)
368
413
  .callbacks(...)
369
414
  .craft();
370
415
  ```
@@ -384,14 +429,16 @@ Each error is defined as a function called an **ErrorDefinition**:
384
429
  The `as const` assertion is **required** for proper TypeScript inference. It ensures your error types are treated as literal types rather than generic:
385
430
 
386
431
  ```typescript
387
- // ❌ Without 'as const' - TypeScript infers { type: string, message: string }
432
+ // ❌ Without 'as const' - TypeScript infers { type: string, message: string } :(
388
433
  badErrorDefinition: () => ({ type: "ERROR", message: "Something went wrong" });
389
434
 
390
- // ✅ With 'as const' - TypeScript infers { type: "ERROR", message: "Something went wrong" }
435
+ // ✅ With 'as const' - TypeScript infers { type: "ERROR", message: "Something went wrong" } :D
391
436
  goodErrorDefinition: () =>
392
437
  ({ type: "ERROR", message: "Something went wrong" }) as const;
393
438
  ```
394
439
 
440
+ Always remember the `as const` assertion when you define your errors!
441
+
395
442
  #### Reusing Common Errors
396
443
 
397
444
  Since error definitions are just functions, you can easily share common errors between actions:
@@ -420,32 +467,34 @@ export const notFound = (resource: string, id: string) =>
420
467
  ```
421
468
 
422
469
  ```typescript
423
- // my-action.ts
424
- export const action = create()
470
+ // get-user.ts
471
+ export const getUser = action()
472
+ .config(...)
425
473
  .schemas(...)
426
474
  .errors({
427
475
  // Easily use common shared errors
428
476
  unauthorized,
429
477
  rateLimited,
430
478
  notFound,
431
- // Plus any action-specific errors
479
+ // Plus any action-specific errors you need
432
480
  emailTaken: (email: string) =>
433
481
  ({ type: "EMAIL_TAKEN", email }) as const,
434
482
  })
435
- .action(...)
483
+ .handler(...)
436
484
  .callbacks(...)
437
485
  .craft();
438
486
  ```
439
487
 
440
- #### Using Errors in Your Action
488
+ #### Using Errors in Your Action Handler
441
489
 
442
- Once defined, you can use these errors in your action logic. When an error occurs, just call and return that particular error function:
490
+ Once defined, you can use these errors in your handler logic. When an error occurs, just call and return that particular error function:
443
491
 
444
492
  ```typescript
445
- const action = create()
493
+ export const getUser = action()
494
+ .config(...)
446
495
  .schemas(...)
447
496
  .errors(...)
448
- .action(async ({ input, errors }) => {
497
+ .handler(async ({ input, errors }) => {
449
498
  // Check permissions
450
499
  if (!hasPermission(input.userId)) {
451
500
  return errors.unauthorized();
@@ -464,22 +513,23 @@ const action = create()
464
513
  .craft();
465
514
  ```
466
515
 
467
- ### .action()
516
+ ### .handler()
468
517
 
469
- The `action` method is where you implement the core functionality of your server action. ActionCraft provides several helpful parameters to make things quick and easy:
518
+ The `handler` method is where you implement the core functionality of your server action. Actioncraft provides several helpful parameters to make things quick and easy for you:
470
519
 
471
520
  ```typescript
472
- const action = create()
521
+ export const getUser = action()
522
+ .config(...)
473
523
  .schemas(...)
474
524
  .errors(...)
475
- .action(async ({ input, bindArgs, errors, metadata }) => {
476
- // Action logic here
525
+ .handler(async ({ input, bindArgs, errors, metadata }) => {
526
+ // Server action logic here
477
527
  })
478
528
  .callbacks(...)
479
529
  .craft();
480
530
  ```
481
531
 
482
- #### Action Parameters
532
+ #### Handler Parameters
483
533
 
484
534
  ##### `input`
485
535
 
@@ -500,16 +550,18 @@ Contains additional request information:
500
550
  - `rawInput`: The original, unvalidated input data
501
551
  - `rawBindArgs`: The original, unvalidated bound arguments array
502
552
  - `prevState`: Previous state (when using `useActionState`)
553
+ - `actionId`: A unique identifier for the action instance
503
554
 
504
555
  ### .callbacks()
505
556
 
506
557
  Sometimes you need to hook into the action lifecycle for logging, analytics, or other side effects. The `callbacks` method lets you define functions that run at key moments:
507
558
 
508
559
  ```typescript
509
- const action = create()
560
+ export const getUser = action()
561
+ .config(...)
510
562
  .schemas(...)
511
563
  .errors(...)
512
- .action(...)
564
+ .handler(...)
513
565
  .callbacks({
514
566
  onStart: ({metadata}) => { ... },
515
567
  onSuccess: ({data}) => { ... },
@@ -527,7 +579,7 @@ Executes first, before any validation or action logic has occurred.
527
579
 
528
580
  ##### `onSuccess?: (params: { data, metadata }) => Promise<void> | void`
529
581
 
530
- Executes when your action completes successfully. The `data` parameter contains your action's return value.
582
+ Executes when your action completes successfully. The `data` parameter contains your action's typed return value.
531
583
 
532
584
  ##### `onError?: (params: { error, metadata }) => Promise<void> | void`
533
585
 
@@ -539,44 +591,32 @@ Executes after your action completes, regardless of success or failure. Useful f
539
591
 
540
592
  Note: All callback methods support async operations and won't affect your action's result, even if they throw errors.
541
593
 
542
- ### .craft()
543
-
544
- Every action should end with the `craft` method. It doesn't take any arguments or anything like that. Its only purpose is to build the final action based on everything that has been defined in the previous methods.
545
-
546
- ```typescript
547
- const action = create()
548
- .schemas(...)
549
- .errors(...)
550
- .action(...)
551
- .callbacks(...)
552
- .craft(); // Returns your fully-typed server action
553
- ```
554
-
555
- This is the final step in the builder chain. Once you call `craft()`, you have a complete, type-safe server action ready to export and use in your application.
556
-
557
594
  ## Using Your Actions
558
595
 
559
- Now that you know how to build actions with ActionCraft, let's see how you can use them in your application.
596
+ Now that you know how to build actions with Actioncraft, let's see how you can use them in your application.
560
597
 
561
598
  ### Basic Usage
562
599
 
563
600
  You can call your action like any async function:
564
601
 
565
602
  ```typescript
566
- const result = await createNewUser({
567
- name: "John",
568
- email: "john@example.com",
569
- age: 25,
570
- });
603
+ // client-component.ts
604
+ const handleClick = async () => {
605
+ const result = await createNewUser({
606
+ name: "John",
607
+ email: "john@example.com",
608
+ age: 25,
609
+ });
571
610
 
572
- if (result.success) {
573
- // Action succeeded
574
- console.log("User created:", result.data.newUser);
575
- } else {
576
- // Action failed
577
- console.log("Error:", result.error.type);
578
- console.log("Message:", result.error.message);
579
- }
611
+ if (result.success) {
612
+ // Action succeeded
613
+ console.log("User created:", result.data.newUser);
614
+ } else {
615
+ // Action failed
616
+ console.log("Error:", result.error.type);
617
+ console.log("Message:", result.error.message);
618
+ }
619
+ };
580
620
  ```
581
621
 
582
622
  ### Error Handling
@@ -609,10 +649,11 @@ if (!result.success) {
609
649
  For React forms, you can use actions configured for `useActionState`:
610
650
 
611
651
  ```typescript
612
- const updateUser = create({ useActionState: true })
652
+ export const updateUser = action()
653
+ .config({ useActionState: true })
613
654
  .schemas(...)
614
655
  .errors(...)
615
- .action(...)
656
+ .handler(...)
616
657
  .callbacks(...)
617
658
  .craft();
618
659
  ```
@@ -621,7 +662,7 @@ When `useActionState: true` is set, your action's return type changes to include
621
662
 
622
663
  #### The `initial()` Helper
623
664
 
624
- When using `useActionState`, you have to provide the hook with a proper initial state that matches the return type of your action. That's where ActionCraft's `initial` function comes in. It returns a special error object with type `"INITIAL_STATE"` that you can use to detect when the form hasn't been submitted yet:
665
+ When using `useActionState`, you have to provide the hook with a proper initial state that matches the return type of your action. That's where Actioncraft's `initial` function comes in. It returns a special error object with type `"INITIAL_STATE"` that you can use to detect when the form hasn't been submitted yet:
625
666
 
626
667
  ```typescript
627
668
  function UserForm() {
@@ -652,14 +693,15 @@ By providing a schema which supports FormData, your action can work with or with
652
693
 
653
694
  ```typescript
654
695
  // This action handles FormData from server-side form submissions
655
- const serverAction = create({ useActionState: true })
696
+ export const createNewUser = action()
697
+ .config({ useActionState: true })
656
698
  .schemas({
657
699
  inputSchema: zfd.formData({
658
700
  name: zfd.text(),
659
701
  email: zfd.text(z.string().email()),
660
702
  }),
661
703
  })
662
- .action(async ({ input }) => {
704
+ .handler(async ({ input }) => {
663
705
  // Save the validated user data to database
664
706
  const user = await db.user.create({
665
707
  data: {
@@ -678,12 +720,12 @@ const serverAction = create({ useActionState: true })
678
720
 
679
721
  ## Complete Example
680
722
 
681
- Now that we've gone over how to create actions and how to use them on the client, let's check out an example that puts a lot of these ideas together:
723
+ Now that we've gone over how to create actions and how to use them on the client, let's check out a more thorough example that puts a lot of these ideas together:
682
724
 
683
725
  ```typescript
684
726
  "use server";
685
727
 
686
- import { create } from "@kellanjs/actioncraft";
728
+ import { action } from "@kellanjs/actioncraft";
687
729
  import { revalidatePath } from "next/cache";
688
730
  import { z } from "zod";
689
731
 
@@ -693,7 +735,8 @@ const updateProfileSchema = z.object({
693
735
  bio: z.string().max(500, "Bio must be under 500 characters"),
694
736
  });
695
737
 
696
- export const updateProfile = create({ useActionState: true })
738
+ export const updateProfile = action()
739
+ .config({ useActionState: true })
697
740
  .schemas({ inputSchema: updateProfileSchema })
698
741
  .errors({
699
742
  unauthorized: () =>
@@ -710,7 +753,7 @@ export const updateProfile = create({ useActionState: true })
710
753
  message: "Too many requests. Please try again later.",
711
754
  }) as const,
712
755
  })
713
- .action(async ({ input, errors }) => {
756
+ .handler(async ({ input, errors }) => {
714
757
  // Check authentication
715
758
  const session = await getSession();
716
759
  if (!session) return errors.unauthorized();
@@ -823,64 +866,282 @@ export default function ProfileForm() {
823
866
  }
824
867
  ```
825
868
 
826
- ## Integrations
869
+ ## Advanced Features
870
+
871
+ ### Bind Arguments
872
+
873
+ Actioncraft supports binding arguments to actions. Just provide schemas, and you'll get the validated bindArgs values to use in the action handler.
874
+
875
+ If validation fails, an error with type `BIND_ARGS_VALIDATION` is returned to the client.
876
+
877
+ #### Example: Multi-Tenant Action
878
+
879
+ ```typescript
880
+ export const createPost = action()
881
+ .schemas({
882
+ bindSchemas: [z.string()], // Organization ID
883
+ inputSchema: z.object({
884
+ title: z.string(),
885
+ content: z.string(),
886
+ }),
887
+ })
888
+ .handler(async ({ bindArgs, input }) => {
889
+ const [organizationId] = bindArgs;
890
+
891
+ const post = await db.post.create({
892
+ data: {
893
+ ...input,
894
+ organizationId,
895
+ },
896
+ });
897
+
898
+ return { post };
899
+ })
900
+ .craft();
901
+
902
+ // Create organization-specific actions
903
+ const createPostForOrgA = createPost.bind(null, "org-a-id");
904
+ const createPostForOrgB = createPost.bind(null, "org-b-id");
905
+
906
+ // Each bound action automatically includes the correct org ID
907
+ const result = await createPostForOrgA({
908
+ title: "My Post",
909
+ content: "Post content...",
910
+ });
911
+ ```
912
+
913
+ #### Example: Configuration Binding
914
+
915
+ ```typescript
916
+ export const sendEmail = action()
917
+ .schemas({
918
+ bindSchemas: [
919
+ z.object({
920
+ apiKey: z.string(),
921
+ fromEmail: z.string(),
922
+ }),
923
+ ],
924
+ inputSchema: z.object({
925
+ to: z.string().email(),
926
+ subject: z.string(),
927
+ body: z.string(),
928
+ }),
929
+ })
930
+ .handler(async ({ bindArgs, input }) => {
931
+ const [config] = bindArgs;
932
+
933
+ // Use the bound configuration
934
+ const emailService = new EmailService(config.apiKey);
935
+ const result = await emailService.send({
936
+ from: config.fromEmail,
937
+ to: input.to,
938
+ subject: input.subject,
939
+ body: input.body,
940
+ });
941
+
942
+ return { messageId: result.id };
943
+ })
944
+ .craft();
945
+
946
+ // Create environment-specific email actions
947
+ const sendProductionEmail = sendEmail.bind(null, {
948
+ apiKey: process.env.PROD_EMAIL_API_KEY,
949
+ fromEmail: "noreply@company.com",
950
+ });
951
+
952
+ const sendDevelopmentEmail = sendEmail.bind(null, {
953
+ apiKey: process.env.DEV_EMAIL_API_KEY,
954
+ fromEmail: "dev@company.com",
955
+ });
956
+ ```
957
+
958
+ ## Utilities
959
+
960
+ Actioncraft provides several utilities to help you work with your actions more effectively.
827
961
 
828
- ### Utilities
962
+ ### Type Inference
829
963
 
830
- ActionCraft comes with several utilities intended to make it easier to integrate with libraries like React Query. Let's take a quick look.
964
+ These utilities extract useful type information from your actions.
831
965
 
832
- #### `ActionCraftError`
966
+ #### Using `$Infer`
833
967
 
834
- A standard Error class that wraps ActionCraft error data while preserving type information:
968
+ Every crafted action includes an `$Infer` property that provides direct access to all inferred types:
969
+
970
+ - **`$Infer.Input`** - The input type that the action expects
971
+ - **`$Infer.Data`** - The success data type from your action's return value
972
+ - **`$Infer.Errors`** - All possible error types your action can return
973
+ - **`$Infer.Result`** - The complete result type (success and error cases)
974
+
975
+ #### Type Extraction Example
976
+
977
+ ```typescript
978
+ import { action } from "@kellanjs/actioncraft";
979
+ import { z } from "zod";
980
+
981
+ export const updateUser = action()
982
+ .schemas({
983
+ inputSchema: z.object({
984
+ id: z.string(),
985
+ name: z.string(),
986
+ email: z.string().email(),
987
+ }),
988
+ })
989
+ .errors({
990
+ notFound: (id: string) => ({ type: "NOT_FOUND", id }) as const,
991
+ unauthorized: () => ({ type: "UNAUTHORIZED" }) as const,
992
+ })
993
+ .handler(async ({ input, errors }) => {
994
+ // ... implementation
995
+ return { user: input, updatedAt: new Date() };
996
+ })
997
+ .craft();
998
+
999
+ // Extracted types using $Infer:
1000
+ type ActionInput = typeof updateUser.$Infer.Input;
1001
+ // { id: string, name: string, email: string }
1002
+
1003
+ type ActionData = typeof updateUser.$Infer.Data;
1004
+ // { user: { id: string, name: string, email: string }, updatedAt: Date }
1005
+
1006
+ type ActionErrors = typeof updateUser.$Infer.Errors;
1007
+ // { type: "NOT_FOUND", id: string } | { type: "UNAUTHORIZED" } |
1008
+ // { type: "INPUT_VALIDATION", issues: ... } | { type: "UNHANDLED", message: string }
1009
+
1010
+ type ActionResult = typeof updateUser.$Infer.Result;
1011
+ // { success: true, data: { user: UserInput, updatedAt: Date } } |
1012
+ // { success: false, error: { type: "NOT_FOUND", id: string } | ... }
1013
+ ```
1014
+
1015
+ #### Using `Infer` Types
1016
+
1017
+ You can also use these alternative type inference utilities if you prefer:
1018
+
1019
+ ```typescript
1020
+ import type {
1021
+ InferInput,
1022
+ InferResult,
1023
+ InferData,
1024
+ InferErrors,
1025
+ } from "@kellanjs/actioncraft";
1026
+
1027
+ type ActionInput = InferInput<typeof updateUser>;
1028
+ type ActionResult = InferResult<typeof updateUser>;
1029
+ type ActionData = InferData<typeof updateUser>;
1030
+ type ActionErrors = InferErrors<typeof updateUser>;
1031
+ ```
1032
+
1033
+ These provide the exact same type information as the `$Infer` utility.
1034
+
1035
+ ### Input Validation
1036
+
1037
+ #### Using `$validate`
1038
+
1039
+ Actioncraft provides a utility to help you easily validate data against a particular action's input schema. The `$validate` method is available on every crafted action by default, and runs the same validation logic used during action execution. This is especially useful when you want to perform client-side validation before calling an action:
1040
+
1041
+ ```typescript
1042
+ // On the server...
1043
+ export const createUser = action()
1044
+ .schemas({ inputSchema: userSchema })
1045
+ .handler(async ({ input }) => ({ user: input }))
1046
+ .craft();
1047
+
1048
+ // On the client...
1049
+ // Validate input without executing the action
1050
+ const result = await createUser.$validate({
1051
+ name: "John",
1052
+ email: "john@example.com",
1053
+ age: 25,
1054
+ });
1055
+
1056
+ if (result.success) {
1057
+ console.log("Valid input:", result.data);
1058
+ // Now we can call the action, knowing that input validation will succeed
1059
+ } else {
1060
+ console.log("Validation failed:", result.error);
1061
+ }
1062
+ ```
1063
+
1064
+ #### Validation Results
1065
+
1066
+ Returns `{ success: true, data: ValidatedInput }` on success, or `{ success: false, error: ValidationError }` on failure.
1067
+
1068
+ ## Integration Utilities
1069
+
1070
+ Actioncraft comes with several utilities intended to make it easier to integrate with libraries like React Query.
1071
+
1072
+ ### Actioncraft Errors
1073
+
1074
+ #### `ActioncraftError`
1075
+
1076
+ A standard Error class that wraps Actioncraft error data while preserving type information:
835
1077
 
836
1078
  ```typescript
837
1079
  // The error preserves all your action's error data in the `cause` property
838
- if (error instanceof ActionCraftError) {
839
- console.log(error.message); // "ActionCraft Error: EMAIL_TAKEN - Email already exists"
1080
+ if (error instanceof ActioncraftError) {
1081
+ console.log(error.message); // "Actioncraft Error: EMAIL_TAKEN - Email already exists"
840
1082
  console.log(error.cause); // { type: "EMAIL_TAKEN", message: "Email already exists", email: "user@example.com" }
841
1083
  }
842
1084
  ```
843
1085
 
844
1086
  #### `unwrap(result)`
845
1087
 
846
- Extracts the data from a successful result or throws an `ActionCraftError`:
1088
+ Extracts the data from a successful result or throws an `ActioncraftError`:
847
1089
 
848
1090
  ```typescript
849
- const result = await myAction(data);
1091
+ const result = await createNewUser(data);
850
1092
  const userData = unwrap(result); // Throws if result.success === false
851
1093
  ```
852
1094
 
853
1095
  #### `throwable(action)`
854
1096
 
855
- Wraps an action to automatically throw errors as `ActionCraftError` instances instead of returning them:
1097
+ Wraps an action to automatically throw errors as `ActioncraftError` instances instead of returning them as objects:
856
1098
 
857
1099
  ```typescript
858
1100
  const throwingAction = throwable(myAction);
859
1101
  const userData = await throwingAction(data); // Throws on error
860
1102
  ```
861
1103
 
862
- #### `isActionCraftError(error, action)`
1104
+ #### `isActioncraftError(error, action)`
863
1105
 
864
- Type guard that enables full type inference for your action's specific error types:
1106
+ Type guard that checks if an error is an `ActioncraftError`. When called with just an error object, it performs basic structural validation. When called with both error and action, it additionally verifies that the error originated from that specific action, providing full type inference for that action's error types.
865
1107
 
866
1108
  ```typescript
867
1109
  try {
868
1110
  const data = await throwable(updateUser)(userData);
1111
+ console.log("Updated user data", data); // We know data exists at this point
869
1112
  } catch (error) {
870
- if (isActionCraftError(error, updateUser)) {
871
- // error.cause is now typed with updateUser's possible error types
1113
+ // Basic usage - checks if error is any ActioncraftError
1114
+ if (isActioncraftError(error)) {
1115
+ console.log("This is an ActioncraftError:", error.cause.type);
1116
+ // error.cause has generic typing here
1117
+ }
1118
+
1119
+ // Advanced usage - verifies error came from the given action
1120
+ if (isActioncraftError(error, updateUser)) {
1121
+ // error.cause is now typed with updateUser's specific error types
872
1122
  switch (error.cause.type) {
873
- case "EMAIL_TAKEN": // Fully typed
874
- case "UNAUTHORIZED": // Fully typed
875
- case "INPUT_VALIDATION": // Fully typed
876
- // Handle each error type
1123
+ case "EMAIL_TAKEN":
1124
+ showError(`Email ${error.cause.email} is already taken`);
1125
+ break;
1126
+ case "UNAUTHORIZED":
1127
+ redirectToLogin();
1128
+ break;
1129
+ case "INPUT_VALIDATION":
1130
+ showValidationErrors(error.cause.issues);
877
1131
  break;
878
1132
  }
879
1133
  }
880
1134
  }
881
1135
  ```
882
1136
 
883
- **Note:** The `action` argument is required for proper type inference - it tells TypeScript which action's error types to expect.
1137
+ **Key Differences:**
1138
+
1139
+ - **Without action parameter**: Performs basic structural validation, returns `true` for any `ActioncraftError`
1140
+ - **With action parameter**: Additionally verifies the error originated from that specific action and provides full type inference for that action's error types
1141
+
1142
+ #### `getActionId(action)`
1143
+
1144
+ Utility to extract the unique ID from a crafted action. Useful for debugging and logging purposes.
884
1145
 
885
1146
  ### React Query
886
1147
 
@@ -900,14 +1161,14 @@ function UserProfile({ userId }: { userId: string }) {
900
1161
  queryKey: ["user", userId],
901
1162
  queryFn: async () => {
902
1163
  const result = await fetchUserProfile({ userId });
903
- return unwrap(result); // Throws ActionCraftError on failure
1164
+ return unwrap(result); // Throws ActioncraftError on failure
904
1165
  },
905
1166
  });
906
1167
 
907
1168
  if (isLoading) return <div>Loading...</div>;
908
1169
 
909
1170
  if (error) {
910
- if (isActionCraftError(error, fetchUserProfile)) {
1171
+ if (isActioncraftError(error, fetchUserProfile)) {
911
1172
  // Full type inference for your action's specific error types
912
1173
  switch (error.cause.type) {
913
1174
  case "USER_NOT_FOUND":
@@ -945,19 +1206,19 @@ Use the `throwable()` utility for mutations:
945
1206
  ```typescript
946
1207
  import { useMutation, useQueryClient } from "@tanstack/react-query";
947
1208
  import { updateUserProfile } from "./actions";
948
- import { throwable, isActionCraftError } from "@kellanjs/actioncraft";
1209
+ import { throwable, isActioncraftError } from "@kellanjs/actioncraft";
949
1210
 
950
1211
  function EditProfileForm() {
951
1212
  const queryClient = useQueryClient();
952
1213
 
953
1214
  const mutation = useMutation({
954
- mutationFn: throwable(updateUserProfile), // Throws ActionCraftError on failure
1215
+ mutationFn: throwable(updateUserProfile), // Throws ActioncraftError on failure
955
1216
  onSuccess: (data) => {
956
1217
  // data is properly typed as your action's success data
957
1218
  queryClient.invalidateQueries({ queryKey: ["user", data.user.id] });
958
1219
  },
959
1220
  onError: (error) => {
960
- if (isActionCraftError(error, updateUserProfile)) {
1221
+ if (isActioncraftError(error, updateUserProfile)) {
961
1222
  // Handle specific error types with full type safety
962
1223
  switch (error.cause.type) {
963
1224
  case "UNAUTHORIZED":
@@ -993,162 +1254,10 @@ function EditProfileForm() {
993
1254
  }
994
1255
  ```
995
1256
 
996
- ## Advanced Features
997
-
998
- ### Bind Arguments
999
-
1000
- ActionCraft supports binding arguments to actions. Just provide schemas, and you'll get the validated bindArgs values to use in the action.
1001
-
1002
- If validation fails, an error with type "BIND_ARGS_VALIDATION" is returned to the client.
1257
+ ## Thanks
1003
1258
 
1004
- #### Example: Multi-Tenant Action
1005
-
1006
- ```typescript
1007
- const createPost = create()
1008
- .schemas({
1009
- bindSchemas: [z.string()], // Organization ID
1010
- inputSchema: z.object({
1011
- title: z.string(),
1012
- content: z.string(),
1013
- }),
1014
- })
1015
- .action(async ({ bindArgs, input }) => {
1016
- const [organizationId] = bindArgs;
1017
-
1018
- const post = await db.post.create({
1019
- data: {
1020
- ...input,
1021
- organizationId,
1022
- },
1023
- });
1024
-
1025
- return { post };
1026
- })
1027
- .craft();
1028
-
1029
- // Create organization-specific actions
1030
- const createPostForOrgA = createPost.bind(null, "org-a-id");
1031
- const createPostForOrgB = createPost.bind(null, "org-b-id");
1032
-
1033
- // Each bound action automatically includes the correct org ID
1034
- const result = await createPostForOrgA({
1035
- title: "My Post",
1036
- content: "Post content...",
1037
- });
1038
- ```
1039
-
1040
- #### Example: Configuration Binding
1041
-
1042
- ```typescript
1043
- const sendEmail = create()
1044
- .schemas({
1045
- bindSchemas: [
1046
- z.object({
1047
- apiKey: z.string(),
1048
- fromEmail: z.string(),
1049
- }),
1050
- ],
1051
- inputSchema: z.object({
1052
- to: z.string().email(),
1053
- subject: z.string(),
1054
- body: z.string(),
1055
- }),
1056
- })
1057
- .action(async ({ bindArgs, input }) => {
1058
- const [config] = bindArgs;
1059
-
1060
- // Use the bound configuration
1061
- const emailService = new EmailService(config.apiKey);
1062
- const result = await emailService.send({
1063
- from: config.fromEmail,
1064
- to: input.to,
1065
- subject: input.subject,
1066
- body: input.body,
1067
- });
1068
-
1069
- return { messageId: result.id };
1070
- })
1071
- .craft();
1072
-
1073
- // Create environment-specific email actions
1074
- const sendProductionEmail = sendEmail.bind(null, {
1075
- apiKey: process.env.PROD_EMAIL_API_KEY,
1076
- fromEmail: "noreply@company.com",
1077
- });
1078
-
1079
- const sendDevelopmentEmail = sendEmail.bind(null, {
1080
- apiKey: process.env.DEV_EMAIL_API_KEY,
1081
- fromEmail: "dev@company.com",
1082
- });
1083
- ```
1084
-
1085
- ## Type Inference Utilities
1086
-
1087
- ActionCraft provides several type inference utilities to extract types from your actions for use elsewhere in your application:
1088
-
1089
- ### Available Utilities
1090
-
1091
- #### `InferInput<Action>`
1092
-
1093
- Infers the input type that the action expects. Returns `unknown` if no input schema is defined.
1094
-
1095
- #### `InferResult<Action>`
1096
-
1097
- Infers the complete result type, including both success and error cases. Respects your action's configuration (api/functional format, useActionState, etc.).
1098
-
1099
- #### `InferData<Action>`
1100
-
1101
- Infers the success data type from your action's return value.
1102
-
1103
- #### `InferErrors<Action>`
1104
-
1105
- Infers all possible error types your action can return, including custom errors, validation errors, and thrown errors.
1106
-
1107
- ### Type Extraction Example
1108
-
1109
- ```typescript
1110
- import { create } from "@kellanjs/actioncraft";
1111
- import type {
1112
- InferInput,
1113
- InferResult,
1114
- InferData,
1115
- InferErrors,
1116
- } from "@kellanjs/actioncraft";
1117
- import { z } from "zod";
1118
-
1119
- const updateUser = create()
1120
- .schemas({
1121
- inputSchema: z.object({
1122
- id: z.string(),
1123
- name: z.string(),
1124
- email: z.string().email(),
1125
- }),
1126
- })
1127
- .errors({
1128
- notFound: (id: string) => ({ type: "NOT_FOUND", id }) as const,
1129
- unauthorized: () => ({ type: "UNAUTHORIZED" }) as const,
1130
- })
1131
- .action(async ({ input, errors }) => {
1132
- // ... implementation
1133
- return { user: input, updatedAt: new Date() };
1134
- })
1135
- .craft();
1136
-
1137
- type ActionInput = InferInput<typeof updateUser>;
1138
- // { id: string, name: string, email: string }
1139
-
1140
- type ActionResult = InferResult<typeof updateUser>;
1141
- // { success: true, data: { user: UserInput, updatedAt: Date } } |
1142
- // { success: false, error: { type: "NOT_FOUND", id: string } | ... }
1143
-
1144
- type ActionData = InferData<typeof updateUser>;
1145
- // { user: { id: string, name: string, email: string }, updatedAt: Date }
1146
-
1147
- type ActionErrors = InferErrors<typeof updateUser>;
1148
- // { type: "NOT_FOUND", id: string } | { type: "UNAUTHORIZED" } |
1149
- // { type: "INPUT_VALIDATION", issues: ... } | { type: "UNHANDLED", message: string }
1150
- ```
1259
+ If you made it this far, thanks for checking out the library, and I hope you find it useful in your projects!
1151
1260
 
1152
1261
  ## License
1153
1262
 
1154
- ActionCraft is open source under the terms of the [MIT license](https://github.com/kellanjs/actioncraft/blob/main/LICENSE).
1263
+ Actioncraft is open source under the terms of the [MIT license](https://github.com/kellanjs/actioncraft/blob/main/LICENSE).