march-hare 0.12.1 → 0.13.1

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 (132) hide show
  1. package/README.md +66 -25
  2. package/dist/action/index.d.ts +2 -2
  3. package/dist/action/utils.d.ts +2 -2
  4. package/dist/actions/index.d.ts +2 -2
  5. package/dist/actions/types.d.ts +3 -3
  6. package/dist/actions/utils.d.ts +3 -3
  7. package/dist/app/index.d.ts +33 -87
  8. package/dist/app/types.d.ts +79 -26
  9. package/dist/boundary/components/broadcast/index.d.ts +2 -2
  10. package/dist/boundary/components/broadcast/types.d.ts +1 -1
  11. package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
  12. package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
  13. package/dist/boundary/components/consumer/index.d.ts +5 -5
  14. package/dist/boundary/components/consumer/types.d.ts +1 -1
  15. package/dist/boundary/components/consumer/utils.d.ts +1 -1
  16. package/dist/boundary/components/env/index.d.ts +3 -16
  17. package/dist/boundary/components/env/types.d.ts +24 -2
  18. package/dist/boundary/components/env/utils.d.ts +1 -1
  19. package/dist/boundary/components/scope/index.d.ts +2 -2
  20. package/dist/boundary/components/scope/types.d.ts +1 -1
  21. package/dist/boundary/components/scope/utils.d.ts +1 -1
  22. package/dist/boundary/components/sharing/index.d.ts +3 -3
  23. package/dist/boundary/components/tap/index.d.ts +3 -3
  24. package/dist/boundary/components/tap/types.d.ts +2 -2
  25. package/dist/boundary/components/tap/utils.d.ts +1 -1
  26. package/dist/boundary/components/tasks/index.d.ts +2 -2
  27. package/dist/boundary/components/tasks/utils.d.ts +1 -1
  28. package/dist/boundary/index.d.ts +3 -3
  29. package/dist/boundary/types.d.ts +3 -3
  30. package/dist/cache/index.d.ts +68 -12
  31. package/dist/cache/types.d.ts +33 -19
  32. package/dist/cli/bin/mh.js +10 -0
  33. package/dist/cli/lib/banner/index.js +14 -0
  34. package/dist/cli/lib/commands/app/index.js +37 -0
  35. package/dist/cli/lib/commands/feature/index.js +55 -0
  36. package/dist/cli/lib/commands/index.js +89 -0
  37. package/dist/cli/lib/commands/init/index.js +29 -0
  38. package/dist/cli/lib/commands/shared/index.js +56 -0
  39. package/dist/cli/lib/index.js +56 -0
  40. package/dist/cli/lib/parser/index.js +24 -0
  41. package/dist/cli/lib/prompt/index.js +61 -0
  42. package/dist/cli/lib/runner/index.js +46 -0
  43. package/dist/cli/lib/runner/types.js +1 -0
  44. package/dist/cli/lib/runner/utils.js +60 -0
  45. package/dist/cli/lib/types.js +1 -0
  46. package/dist/cli/lib/utils.js +20 -0
  47. package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
  48. package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
  49. package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
  50. package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
  51. package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
  52. package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
  53. package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
  54. package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
  55. package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
  56. package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
  57. package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
  58. package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
  59. package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
  60. package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
  61. package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
  62. package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
  63. package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
  64. package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
  65. package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
  66. package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
  67. package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
  68. package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
  69. package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
  70. package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
  71. package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
  72. package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
  73. package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
  74. package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
  75. package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
  76. package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
  77. package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
  78. package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
  79. package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
  80. package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
  81. package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
  82. package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
  83. package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
  84. package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
  85. package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
  86. package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
  87. package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
  88. package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
  89. package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
  90. package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
  91. package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
  92. package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
  93. package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
  94. package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
  95. package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
  96. package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
  97. package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
  98. package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
  99. package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
  100. package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
  101. package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
  102. package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
  103. package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
  104. package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
  105. package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
  106. package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
  107. package/dist/coalesce/index.d.ts +1 -1
  108. package/dist/context/index.d.ts +2 -2
  109. package/dist/error/index.d.ts +18 -1
  110. package/dist/error/types.d.ts +1 -18
  111. package/dist/error/utils.d.ts +1 -1
  112. package/dist/index.d.ts +16 -14
  113. package/dist/march-hare.js +7 -6
  114. package/dist/march-hare.js.map +1 -0
  115. package/dist/march-hare.umd.cjs +2 -1
  116. package/dist/march-hare.umd.cjs.map +1 -0
  117. package/dist/resource/index.d.ts +32 -61
  118. package/dist/resource/types.d.ts +45 -22
  119. package/dist/resource/utils.d.ts +31 -3
  120. package/dist/scope/index.d.ts +4 -64
  121. package/dist/scope/types.d.ts +8 -8
  122. package/dist/scope/utils.d.ts +12 -0
  123. package/dist/shared/index.d.ts +12 -21
  124. package/dist/types/index.d.ts +114 -29
  125. package/dist/utils/index.d.ts +3 -3
  126. package/dist/utils/types.d.ts +1 -3
  127. package/dist/utils/utils.d.ts +1 -3
  128. package/dist/with/index.d.ts +17 -62
  129. package/dist/with/types.d.ts +66 -0
  130. package/dist/with/utils.d.ts +61 -0
  131. package/package.json +21 -4
  132. package/src/cli/README.md +314 -0
@@ -1,9 +1,9 @@
1
1
  import { Operation, Process, Inspect, Box } from 'immertation';
2
- import { ActionId, Task, Tasks } from '../boundary/components/tasks/types';
3
- import { Fault } from '../error/types';
4
- import { Env } from '../boundary/components/env/index';
5
- import { Coalesce } from '../resource/types';
6
- import { WithHandle } from '../with/index';
2
+ import { ActionId, Task, Tasks } from '../boundary/components/tasks/types.js';
3
+ import { Fault } from '../error/types.js';
4
+ import { Env } from '../boundary/components/env/types.js';
5
+ import { Coalesce, Invocation } from '../resource/types.js';
6
+ import { WithHandle } from '../with/types.js';
7
7
  import * as React from "react";
8
8
  /**
9
9
  * Chainable handle returned from `context.actions.resource(invocation)`.
@@ -17,13 +17,21 @@ import * as React from "react";
17
17
  * Awaiting the handle (`await context.actions.resource(...)`) triggers
18
18
  * the fetch with whichever options have been set on the chain.
19
19
  */
20
- export type ResourceCall<T> = PromiseLike<T> & {
20
+ /**
21
+ * Fetch-configured chain returned from `.exceeds(...)` and
22
+ * `.coalesce(...)`. Awaiting the chain runs the fetch with whichever
23
+ * options are set; `.evict()` is intentionally absent because the
24
+ * "configured a fetch then evicted instead" sequence has no coherent
25
+ * meaning &mdash; eviction is always available off the bare
26
+ * `context.actions.resource(...)` call.
27
+ */
28
+ export type ResourceFetch<T> = PromiseLike<T> & {
21
29
  /**
22
30
  * Skip the fetch when the cached payload is within `duration`.
23
31
  * Accepts a `Temporal.Duration`, a `DurationLike` object
24
32
  * (`{ minutes: 5 }`), or an ISO 8601 string (`"PT5M"`).
25
33
  */
26
- readonly exceeds: (duration: Temporal.DurationLike) => ResourceCall<T>;
34
+ readonly exceeds: (duration: Temporal.DurationLike) => ResourceFetch<T>;
27
35
  /**
28
36
  * Join an in-flight fetch for the same `(resource, params, token)`
29
37
  * tuple. The shared fetch runs against a detached `AbortController`
@@ -34,7 +42,43 @@ export type ResourceCall<T> = PromiseLike<T> & {
34
42
  * `token` is optional &mdash; omit it to share with every other
35
43
  * untokened caller for the same `(resource, params)` slot.
36
44
  */
37
- readonly coalesce: (token?: Coalesce) => ResourceCall<T>;
45
+ readonly coalesce: (token?: Coalesce) => ResourceFetch<T>;
46
+ };
47
+ /**
48
+ * Chainable handle returned from `context.actions.resource(invocation)`.
49
+ * Either resolve to the fetched value (`.exceeds`/`.coalesce` + await)
50
+ * or drop the cache slot (`.evict`) &mdash; the two paths are mutually
51
+ * exclusive, so once `.exceeds` or `.coalesce` runs the chain narrows
52
+ * to {@link ResourceFetch} and `.evict` is no longer available.
53
+ */
54
+ export type ResourceCall<T> = ResourceFetch<T> & {
55
+ /**
56
+ * Drop cache entries for the primed resource without fetching. With
57
+ * no argument, uses the params from the originating call as the
58
+ * pattern. With an argument, evicts every stored entry whose params
59
+ * satisfy the pattern's keys (partial match &mdash; extra keys in
60
+ * the stored params are ignored).
61
+ *
62
+ * Strictly synchronous &mdash; the Adapter contract is sync, so the
63
+ * warm-start `Map` and the user adapter both settle in the current
64
+ * tick. Async backends fire-and-forget their underlying delete from
65
+ * inside the adapter body; the call site doesn't `await` anything.
66
+ *
67
+ * The `where` pattern is typed as `Record<string, unknown>` rather
68
+ * than `Partial<P>` because the resource's params type `P` isn't
69
+ * threaded through the chain. Pass the literal you'd pass to the
70
+ * underlying fetcher &mdash; TypeScript won't catch typos in pattern
71
+ * keys, so prefer the no-argument form when possible.
72
+ *
73
+ * ```ts
74
+ * // Drop the {id: 5} slot.
75
+ * context.actions.resource(resource.user({ id: 5 })).evict();
76
+ *
77
+ * // Drop every user slot whose stored params include name "Adam".
78
+ * context.actions.resource(resource.user()).evict({ name: "Adam" });
79
+ * ```
80
+ */
81
+ readonly evict: (where?: Record<string, unknown>) => void;
38
82
  };
39
83
  export type { ActionId, Box, Task, Tasks };
40
84
  /**
@@ -108,6 +152,18 @@ export declare class Brand {
108
152
  * symbols imported from a class outside `AC`.
109
153
  */
110
154
  static readonly Name: unique symbol;
155
+ /**
156
+ * Phantom brand identifying lifecycle actions returned by
157
+ * `Lifecycle.Mount()`, `Lifecycle.Unmount()`, `Lifecycle.Error()`, and
158
+ * `Lifecycle.Update()`. Carries the lifecycle's literal kind so that
159
+ * `useAction` can pick distinct overloads &mdash; in particular,
160
+ * `Lifecycle.Update` resolves its payload to `Partial<DeepReadonly<D>>`
161
+ * against the surrounding `useActions` data generic instead of the
162
+ * factory-level `Record<string, unknown>` placeholder. Without this
163
+ * brand a user-defined `Action<P>("Update")` would collide with the
164
+ * lifecycle overload.
165
+ */
166
+ static readonly Lifecycle: unique symbol;
111
167
  }
112
168
  /**
113
169
  * Internal symbol for the global `Lifecycle.Fault` broadcast. Exposed so the
@@ -151,13 +207,18 @@ export declare const EnvSymbol: unique symbol;
151
207
  */
152
208
  export declare class Lifecycle {
153
209
  /** Creates a Mount lifecycle action. Triggered once on component mount (`useLayoutEffect`). */
154
- static Mount(): HandlerPayload<never, never, "Mount">;
210
+ static Mount(): LifecyclePayload<never, never, "Mount">;
155
211
  /** Creates an Unmount lifecycle action. Triggered when the component unmounts. */
156
- static Unmount(): HandlerPayload<never, never, "Unmount">;
212
+ static Unmount(): LifecyclePayload<never, never, "Unmount">;
157
213
  /** Creates an Error lifecycle action. Triggered when an action throws. Receives `Fault` as payload. */
158
- static Error(): HandlerPayload<Fault, never, "Error">;
159
- /** Creates an Update lifecycle action. Triggered when `context.data` changes (not on initial mount). */
160
- static Update(): HandlerPayload<Record<string, unknown>, never, "Update">;
214
+ static Error(): LifecyclePayload<Fault, never, "Error">;
215
+ /**
216
+ * Creates an Update lifecycle action. Triggered when `context.data` changes
217
+ * (not on initial mount). The handler payload is typed as
218
+ * `Partial<DeepReadonly<D>>` at the subscription site &mdash; only the keys
219
+ * whose values changed between the previous and current render are present.
220
+ */
221
+ static Update(): LifecyclePayload<Record<string, unknown>, never, "Update">;
161
222
  /**
162
223
  * Global fault broadcast. Receives a `Fault` whenever any action in the
163
224
  * `<Boundary>` errors, times out, or is supplanted. Subscribe via
@@ -324,6 +385,25 @@ export type HandlerPayload<P = unknown, C extends Filter = never, Name extends s
324
385
  } & ([C] extends [never] ? unknown : {
325
386
  (channel: C): ChanneledAction<P, C, Name>;
326
387
  });
388
+ /**
389
+ * Branded type returned by `Lifecycle.Mount`, `Lifecycle.Unmount`,
390
+ * `Lifecycle.Error`, and `Lifecycle.Update`. Structurally identical to a
391
+ * `HandlerPayload` but carries a phantom `Brand.Lifecycle` brand whose value
392
+ * is the lifecycle's literal kind. The brand is what lets `useAction` and
393
+ * `Handlers` resolve `Lifecycle.Update`'s payload to `Partial<DeepReadonly<D>>`
394
+ * (against the surrounding `useActions` data generic) instead of the
395
+ * factory-level `Record<string, unknown>` placeholder &mdash; a user-defined
396
+ * `Action<P>("Update")` would have `Name = "Update"` but no `Brand.Lifecycle`,
397
+ * so it falls into the generic payload overload as expected.
398
+ *
399
+ * @template P Payload type for the lifecycle.
400
+ * @template C Channel filter (always `never` for lifecycles &mdash; they are
401
+ * not channeled).
402
+ * @template Name Literal name (`"Mount"`, `"Unmount"`, `"Error"`, `"Update"`).
403
+ */
404
+ export type LifecyclePayload<P = unknown, C extends Filter = never, Name extends string = string> = HandlerPayload<P, C, Name> & {
405
+ readonly [Brand.Lifecycle]: Name;
406
+ };
327
407
  /**
328
408
  * Result of calling an action with a channel argument.
329
409
  * Contains the action reference and the channel data for filtered dispatch.
@@ -513,25 +593,25 @@ export type Actions = object;
513
593
  export type Result = {
514
594
  processes: Set<Process>;
515
595
  };
516
- export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = {
596
+ export type HandlerContext<M extends Model | void, AC extends Actions | void, D extends Props = Props, E extends Env = Env> = {
517
597
  readonly model: DeepReadonly<M>;
518
598
  readonly phase: Phase;
519
599
  readonly task: Task;
520
600
  readonly data: DeepReadonly<D>;
521
601
  readonly tasks: ReadonlySet<Task>;
522
- readonly env: Readonly<S>;
602
+ readonly env: Readonly<E>;
523
603
  readonly actions: {
524
604
  produce<F extends (draft: {
525
605
  model: M;
526
- env: S;
606
+ env: E;
527
607
  readonly inspect: Readonly<Inspect<M>>;
528
608
  }) => void>(ƒ: F & AssertSync<F>): void;
529
609
  dispatch(action: NoPayloadActions<Dispatchable<AC>>): Promise<void>;
530
610
  dispatch<A extends WithPayloadActions<Dispatchable<AC>>>(action: A, payload: Payload<A>): Promise<void>;
531
611
  annotate<T>(value: T, operation?: Operation): T;
532
612
  readonly inspect: Readonly<Inspect<M>>;
533
- resource: (<T>(invocation: T | null) => ResourceCall<T>) & {
534
- set<T>(invocation: T | null, data: T): void;
613
+ resource: (<T, P extends object>(invocation: Invocation<T, P>) => ResourceCall<T>) & {
614
+ nuke(where?: Record<string, unknown>): void;
535
615
  };
536
616
  final<T>(action: BroadcastPayload<T> | MulticastPayload<T>): Promise<T | null>;
537
617
  peek<T>(action: BroadcastPayload<T> | MulticastPayload<T>): T | null;
@@ -582,7 +662,7 @@ export type HandlerContext<M extends Model | void, AC extends Actions | void, D
582
662
  *
583
663
  * @see {@link Handlers} for the recommended HKT pattern
584
664
  */
585
- export type Handler<M extends Model | void, AC extends Actions | void, K extends keyof AC & string, D extends Props = Props, S extends Env = Env> = (context: HandlerContext<M, AC, D, S>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator;
665
+ export type Handler<M extends Model | void, AC extends Actions | void, K extends keyof AC & string, D extends Props = Props, E extends Env = Env> = (context: HandlerContext<M, AC, D, E>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator;
586
666
  /**
587
667
  * String keys of `AC` excluding inherited `prototype` from class constructors.
588
668
  * When action containers are classes (`typeof MyActions`), TypeScript includes
@@ -674,10 +754,12 @@ export type WithPayloadActions<U> = Exclude<U, {
674
754
  * export const handlePaymentSent: H["Broadcast"]["PaymentSent"] = (context) => { ... };
675
755
  * ```
676
756
  */
677
- export type Handlers<M extends Model | void, AC extends Actions | void, D extends Props = Props, RootAC extends Actions | void = AC, S extends Env = Env> = {
678
- [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? (context: HandlerContext<M, RootAC, D, S>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D, RootAC, S>;
757
+ export type Handlers<M extends Model | void, AC extends Actions | void, D extends Props = Props, RootAC extends Actions | void = AC, E extends Env = Env> = {
758
+ [K in OwnKeys<AC>]: OwnKeys<AC[K]> extends never ? AC[K] extends {
759
+ readonly [Brand.Lifecycle]: "Update";
760
+ } ? (context: HandlerContext<M, RootAC, D, E>, changes: Partial<DeepReadonly<D>>) => void | Promise<void> | AsyncGenerator | Generator : (context: HandlerContext<M, RootAC, D, E>, ...args: [Payload<AC[K] & HandlerPayload<unknown>>] extends [never] ? [] : [payload: Payload<AC[K] & HandlerPayload<unknown>>]) => void | Promise<void> | AsyncGenerator | Generator : Handlers<M, AC[K] & Actions, D, RootAC, E>;
679
761
  };
680
- export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = [
762
+ export type UseActions<M extends Model | void, AC extends Actions | void, D extends Props = Props, E extends Env = Env> = [
681
763
  Readonly<M>,
682
764
  {
683
765
  /**
@@ -710,7 +792,7 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
710
792
  * );
711
793
  * ```
712
794
  */
713
- stream(action: typeof Lifecycle.Env, renderer: (value: Readonly<S>, inspect: Inspect<S>) => React.ReactNode): React.ReactNode;
795
+ stream(action: typeof Lifecycle.Env, renderer: (value: Readonly<E>, inspect: Inspect<E>) => React.ReactNode): React.ReactNode;
714
796
  stream<T extends object>(action: BroadcastPayload<T>, renderer: (value: T, inspect: Inspect<T>) => React.ReactNode): React.ReactNode;
715
797
  },
716
798
  DeepReadonly<D>
@@ -751,9 +833,12 @@ export type UseActions<M extends Model | void, AC extends Actions | void, D exte
751
833
  * });
752
834
  * ```
753
835
  */
754
- useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D, S>) => void | Promise<void> | AsyncGenerator | Generator): void;
755
- useAction(action: typeof Lifecycle.Env, handler: (context: HandlerContext<M, AC, D, S>, env: Readonly<S>) => void | Promise<void> | AsyncGenerator | Generator): void;
756
- useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D, S>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
836
+ useAction(action: NoPayloadActions<Subscribable<AC>>, handler: (context: HandlerContext<M, AC, D, E>) => void | Promise<void> | AsyncGenerator | Generator): void;
837
+ useAction(action: typeof Lifecycle.Env, handler: (context: HandlerContext<M, AC, D, E>, env: Readonly<E>) => void | Promise<void> | AsyncGenerator | Generator): void;
838
+ useAction<A extends Extract<Subscribable<AC>, {
839
+ readonly [Brand.Lifecycle]: "Update";
840
+ }>>(action: A, handler: (context: HandlerContext<M, AC, D, E>, changes: Partial<DeepReadonly<D>>) => void | Promise<void> | AsyncGenerator | Generator): void;
841
+ useAction<A extends WithPayloadActions<Subscribable<AC>>>(action: A, handler: (context: HandlerContext<M, AC, D, E>, payload: Payload<A>) => void | Promise<void> | AsyncGenerator | Generator): void;
757
842
  };
758
843
  /**
759
844
  * Stable, typed dispatch function for the actions class `AC`. Same call
@@ -776,7 +861,7 @@ export type Dispatch<AC extends Actions | void> = {
776
861
  * `React.Context` &mdash; it's the March Hare action surface returned by
777
862
  * the `useContext` hook of this library.
778
863
  */
779
- export type Context<M extends Model | void, AC extends Actions | void, D extends Props = Props, S extends Env = Env> = {
864
+ export type Context<M extends Model | void, AC extends Actions | void, D extends Props = Props, E extends Env = Env> = {
780
865
  readonly actions: {
781
866
  dispatch: Dispatch<AC>;
782
867
  };
@@ -786,6 +871,6 @@ export type Context<M extends Model | void, AC extends Actions | void, D extends
786
871
  * autocomplete from the model. See {@link WithHandle}.
787
872
  */
788
873
  readonly with: WithHandle<M>;
789
- useActions(getData?: () => D): UseActions<M, AC, D, S>;
790
- useActions(model: M extends void ? never : M, getData?: () => D): UseActions<M, AC, D, S>;
874
+ useActions(getData?: () => D): UseActions<M, AC, D, E>;
875
+ useActions(model: M extends void ? never : M, getData?: () => D): UseActions<M, AC, D, E>;
791
876
  };
@@ -1,6 +1,6 @@
1
- import { Pk } from '../types/index';
2
- export { unset } from './utils';
3
- export type { Stored, Unset } from './types';
1
+ import { Pk } from '../types/index.js';
2
+ export { unset } from './utils.js';
3
+ export type { Stored, Unset } from './types.js';
4
4
  /**
5
5
  * Returns a promise that resolves after the specified number of
6
6
  * milliseconds, or rejects with an {@link Aborted} when the signal is aborted. Use to inject a cancellable
@@ -1,4 +1,4 @@
1
- import { unset } from './utils';
1
+ import { unset } from './utils.js';
2
2
  /** Nominal type of the {@link unset} sentinel. */
3
3
  export type Unset = typeof unset;
4
4
  /**
@@ -13,6 +13,4 @@ export type Stored<T> = {
13
13
  readonly data: T | Unset;
14
14
  /** When the payload was recorded, or `null` when nothing is recorded. */
15
15
  readonly at: Temporal.Instant | null;
16
- /** Returns {@link data} when present, otherwise the supplied fallback. */
17
- readonly else: <U>(fallback: U) => T | U;
18
16
  };
@@ -1,4 +1,4 @@
1
- import { Stored } from './types';
1
+ import { Stored } from './types.js';
2
2
  /**
3
3
  * Sentinel symbol marking "no value present yet". Shared by the Resource
4
4
  * cache and by storage handles so callers can distinguish "nothing has been
@@ -19,7 +19,6 @@ export declare function useRerender(): () => void;
19
19
  *
20
20
  * @template T The payload type the resulting Stored would carry if populated.
21
21
  * @returns A Stored with `data` set to {@link unset} and `at` set to `null`.
22
- * Its `.else(fallback)` returns the fallback unchanged.
23
22
  * @internal
24
23
  */
25
24
  export declare function empty<T>(): Stored<T>;
@@ -30,7 +29,6 @@ export declare function empty<T>(): Stored<T>;
30
29
  * @param data The payload value to wrap.
31
30
  * @param at The instant the payload was recorded — flows through to the
32
31
  * Resource cache as the entry's `at` timestamp.
33
- * @returns A Stored whose `.else(fallback)` returns `data` unchanged.
34
32
  * @internal
35
33
  */
36
34
  export declare function present<T>(data: T, at: Temporal.Instant): Stored<T>;
@@ -1,61 +1,6 @@
1
- import { Actions, HandlerContext, Maybe, Model, Props } from '../types/index';
2
- import { Env } from '../boundary/components/env/index';
3
- type Primitive = Maybe<string | number | bigint | boolean | symbol>;
4
- type Depth = [never, 0, 1, 2, 3, 4, 5];
5
- /**
6
- * Lodash-style dotted paths reachable from `T`. Yields `"a"`, `"a.b"`,
7
- * `"items.0"`, `"items.0.name"`, etc. Recursion is capped at depth 5 to
8
- * keep the type-checker tractable on deeply nested models.
9
- *
10
- * @template T The object type to enumerate paths from.
11
- * @template D Recursion budget (internal).
12
- */
13
- export type Paths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? `${number}` | (U extends Primitive ? never : `${number}.${Paths<U, Depth[D]>}`) : T extends object ? {
14
- [K in Extract<keyof T, string>]: T[K] extends Primitive ? K : K | `${K}.${Paths<T[K], Depth[D]>}`;
15
- }[Extract<keyof T, string>] : never;
16
- /**
17
- * Subset of {@link Paths} whose leaf type is `boolean`. Used by
18
- * `context.with.invert` (and the legacy {@link With.Invert}) to restrict the
19
- * key to togglable fields only.
20
- *
21
- * @template T The object type to enumerate boolean leaves from.
22
- * @template D Recursion budget (internal).
23
- */
24
- export type BooleanPaths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? (U extends boolean ? `${number}` : never) | (U extends Primitive ? never : `${number}.${BooleanPaths<U, Depth[D]>}`) : T extends object ? {
25
- [K in Extract<keyof T, string>]: T[K] extends boolean ? K : T[K] extends Primitive ? never : `${K}.${BooleanPaths<T[K], Depth[D]>}`;
26
- }[Extract<keyof T, string>] : never;
27
- /**
28
- * Resolves the leaf type at a dotted path on `T`. `Get<{a:{b:number}},"a.b">`
29
- * is `number`; `Get<{items: string[]},"items.0">` is `string`.
30
- *
31
- * @template T The object type to walk.
32
- * @template P The dotted path string.
33
- */
34
- export type Get<T, P extends string> = P extends `${infer Head}.${infer Tail}` ? T extends ReadonlyArray<infer U> ? Head extends `${number}` ? Get<U, Tail> : never : Head extends keyof T ? Get<T[Head], Tail> : never : T extends ReadonlyArray<infer U> ? P extends `${number}` ? U : never : P extends keyof T ? T[P] : never;
35
- /**
36
- * Returned by `context.with` &mdash; a typed bag of handler factories
37
- * bound to the model `M` declared in `useContext<M, …>()`. Methods accept
38
- * lodash-style dotted paths (`"a.b.c"`) with array indices (`"items.0.id"`).
39
- *
40
- * - `update(key)` &mdash; assigns the dispatched payload to `model[key]`.
41
- * - `invert(key)` &mdash; flips a boolean leaf at `model[key]`.
42
- * - `always(key, value)` &mdash; assigns a fixed `value` to `model[key]`,
43
- * ignoring any dispatched payload.
44
- *
45
- * @template M The model type to bind keys against.
46
- */
47
- export type WithHandle<M> = M extends Model ? {
48
- update<K extends Paths<M>>(key: K): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>, payload: Get<M, K>) => void;
49
- invert<K extends BooleanPaths<M>>(key: K): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>) => void;
50
- always<K extends Paths<M>>(key: K, value: Get<M, K>): <A extends Actions | void, D extends Props, S extends Env = Env>(context: HandlerContext<M, A, D, S>) => void;
51
- } : Record<string, never>;
52
- /**
53
- * Builds the {@link WithHandle} object returned via `context.with`. The
54
- * runtime is identical for any model &mdash; only the call-site types differ.
55
- *
56
- * @internal
57
- */
58
- export declare function bindWith<M extends Model | void>(): WithHandle<M>;
1
+ import { Actions, HandlerContext, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ import { BooleanPaths, Get, Paths } from './types.js';
59
4
  /**
60
5
  * Handler factories that wire an action directly to a model field. Prefer
61
6
  * `context.with` from `useContext<Model>()` for first-class autocompletion
@@ -95,17 +40,27 @@ export declare const With: {
95
40
  * Returns a handler that assigns the action payload to the model leaf at
96
41
  * the given lodash-style path. The payload type must match `Get<M, K>`,
97
42
  * and the path must exist on the model.
43
+ *
44
+ * @template K The dotted-path string indexing into the model.
45
+ * @param key The lodash-style path to the model leaf being assigned.
98
46
  */
99
- Update<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, P extends K extends Paths<M> ? Get<M, K> : never, S extends Env = Env>(context: HandlerContext<M, A, D, S>, payload: P) => void;
47
+ Update<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, P extends K extends Paths<M> ? Get<M, K> : never, E extends Env = Env>(context: HandlerContext<M, A, D, E>, payload: P) => void;
100
48
  /**
101
49
  * Returns a handler that inverts a boolean leaf at the given lodash-style
102
50
  * path. The leaf must be a `boolean` on the model.
51
+ *
52
+ * @template K The dotted-path string indexing into a boolean leaf.
53
+ * @param key The lodash-style path to the boolean leaf being toggled.
103
54
  */
104
- Invert<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, S extends Env = Env>(context: K extends BooleanPaths<M> ? HandlerContext<M, A, D, S> : never) => void;
55
+ Invert<K extends string>(key: K): <M extends Model, A extends Actions | void, D extends Props, E extends Env = Env>(context: K extends BooleanPaths<M> ? HandlerContext<M, A, D, E> : never) => void;
105
56
  /**
106
57
  * Returns a handler that assigns a fixed `value` to the model leaf at the
107
58
  * given lodash-style path. The dispatched payload (if any) is ignored.
59
+ *
60
+ * @template K The dotted-path string indexing into the model.
61
+ * @template V The constant value type; must be assignable to the leaf at `K`.
62
+ * @param key The lodash-style path to the model leaf being assigned.
63
+ * @param value The constant value pinned to the leaf.
108
64
  */
109
- Always<K extends string, V>(key: K, value: V): <M extends Model, A extends Actions | void, D extends Props, S extends Env = Env>(context: K extends Paths<M> ? V extends Get<M, K> ? HandlerContext<M, A, D, S> : never : never) => void;
65
+ Always<K extends string, V>(key: K, value: V): <M extends Model, A extends Actions | void, D extends Props, E extends Env = Env>(context: K extends Paths<M> ? V extends Get<M, K> ? HandlerContext<M, A, D, E> : never : never) => void;
110
66
  };
111
- export {};
@@ -0,0 +1,66 @@
1
+ import { Actions, HandlerContext, Maybe, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ /**
4
+ * Non-nullable primitive leaves that {@link Paths} can terminate on. Used
5
+ * internally to stop the recursive walk at scalar boundaries; consumers
6
+ * shouldn't reach for this directly.
7
+ *
8
+ * @internal
9
+ */
10
+ export type Primitive = Maybe<string | number | bigint | boolean | symbol>;
11
+ /**
12
+ * Decrement table used to cap {@link Paths} / {@link BooleanPaths}
13
+ * recursion depth. Indexing `Depth[N]` produces `N - 1` (or `never`
14
+ * once the budget is exhausted), keeping the type-checker tractable
15
+ * on deeply nested models.
16
+ *
17
+ * @internal
18
+ */
19
+ export type Depth = [never, 0, 1, 2, 3, 4, 5];
20
+ /**
21
+ * Lodash-style dotted paths reachable from `T`. Yields `"a"`, `"a.b"`,
22
+ * `"items.0"`, `"items.0.name"`, etc. Recursion is capped at depth 5 to
23
+ * keep the type-checker tractable on deeply nested models.
24
+ *
25
+ * @template T The object type to enumerate paths from.
26
+ * @template D Recursion budget (internal).
27
+ */
28
+ export type Paths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? `${number}` | (U extends Primitive ? never : `${number}.${Paths<U, Depth[D]>}`) : T extends object ? {
29
+ [K in Extract<keyof T, string>]: T[K] extends Primitive ? K : K | `${K}.${Paths<T[K], Depth[D]>}`;
30
+ }[Extract<keyof T, string>] : never;
31
+ /**
32
+ * Subset of {@link Paths} whose leaf type is `boolean`. Used by
33
+ * `context.with.invert` (and the legacy {@link With.Invert}) to restrict the
34
+ * key to togglable fields only.
35
+ *
36
+ * @template T The object type to enumerate boolean leaves from.
37
+ * @template D Recursion budget (internal).
38
+ */
39
+ export type BooleanPaths<T, D extends number = 5> = [D] extends [never] ? never : T extends Primitive ? never : T extends ReadonlyArray<infer U> ? (U extends boolean ? `${number}` : never) | (U extends Primitive ? never : `${number}.${BooleanPaths<U, Depth[D]>}`) : T extends object ? {
40
+ [K in Extract<keyof T, string>]: T[K] extends boolean ? K : T[K] extends Primitive ? never : `${K}.${BooleanPaths<T[K], Depth[D]>}`;
41
+ }[Extract<keyof T, string>] : never;
42
+ /**
43
+ * Resolves the leaf type at a dotted path on `T`. `Get<{a:{b:number}},"a.b">`
44
+ * is `number`; `Get<{items: string[]},"items.0">` is `string`.
45
+ *
46
+ * @template T The object type to walk.
47
+ * @template P The dotted path string.
48
+ */
49
+ export type Get<T, P extends string> = P extends `${infer Head}.${infer Tail}` ? T extends ReadonlyArray<infer U> ? Head extends `${number}` ? Get<U, Tail> : never : Head extends keyof T ? Get<T[Head], Tail> : never : T extends ReadonlyArray<infer U> ? P extends `${number}` ? U : never : P extends keyof T ? T[P] : never;
50
+ /**
51
+ * Returned by `context.with` &mdash; a typed bag of handler factories
52
+ * bound to the model `M` declared in `useContext<M, …>()`. Methods accept
53
+ * lodash-style dotted paths (`"a.b.c"`) with array indices (`"items.0.id"`).
54
+ *
55
+ * - `update(key)` &mdash; assigns the dispatched payload to `model[key]`.
56
+ * - `invert(key)` &mdash; flips a boolean leaf at `model[key]`.
57
+ * - `always(key, value)` &mdash; assigns a fixed `value` to `model[key]`,
58
+ * ignoring any dispatched payload.
59
+ *
60
+ * @template M The model type to bind keys against.
61
+ */
62
+ export type WithHandle<M> = M extends Model ? {
63
+ update<K extends Paths<M>>(key: K): <A extends Actions | void, D extends Props, E extends Env = Env>(context: HandlerContext<M, A, D, E>, payload: Get<M, K>) => void;
64
+ invert<K extends BooleanPaths<M>>(key: K): <A extends Actions | void, D extends Props, E extends Env = Env>(context: HandlerContext<M, A, D, E>) => void;
65
+ always<K extends Paths<M>>(key: K, value: Get<M, K>): <A extends Actions | void, D extends Props, E extends Env = Env>(context: HandlerContext<M, A, D, E>) => void;
66
+ } : Record<string, never>;
@@ -0,0 +1,61 @@
1
+ import { Actions, HandlerContext, Model, Props } from '../types/index.js';
2
+ import { Env } from '../boundary/components/env/types.js';
3
+ import { WithHandle } from './types.js';
4
+ /**
5
+ * Walks the lodash-style dotted `path` on `target`, stopping one segment
6
+ * short so callers can mutate or read the leaf via the returned `cursor`
7
+ * and `key`. Used by every assignment helper below to avoid duplicating
8
+ * the per-segment descent.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare function walk(target: unknown, path: string): {
13
+ cursor: Record<string, unknown>;
14
+ key: string;
15
+ };
16
+ /**
17
+ * Assigns `value` to the leaf of `target` reached by lodash-style `path`.
18
+ * Mutates in place &mdash; expected to be called from inside an Immer
19
+ * `produce` draft.
20
+ *
21
+ * @internal
22
+ */
23
+ export declare function setPath(target: unknown, path: string, value: unknown): void;
24
+ /**
25
+ * Flips the boolean leaf of `target` reached by lodash-style `path`.
26
+ * Mutates in place &mdash; expected to be called from inside an Immer
27
+ * `produce` draft.
28
+ *
29
+ * @internal
30
+ */
31
+ export declare function invertPath(target: unknown, path: string): void;
32
+ /**
33
+ * Returns a handler that assigns the dispatched payload to the model
34
+ * leaf at lodash-style `key`. Underlies both `context.with.update` and
35
+ * the top-level `With.Update`.
36
+ *
37
+ * @internal
38
+ */
39
+ export declare function makeUpdate(key: string): (context: HandlerContext<Model, Actions, Props, Env>, payload: unknown) => void;
40
+ /**
41
+ * Returns a handler that flips the boolean model leaf at lodash-style
42
+ * `key`. Underlies both `context.with.invert` and `With.Invert`.
43
+ *
44
+ * @internal
45
+ */
46
+ export declare function makeInvert(key: string): (context: HandlerContext<Model, Actions, Props, Env>) => void;
47
+ /**
48
+ * Returns a handler that assigns the constant `value` to the model
49
+ * leaf at lodash-style `key`, ignoring any dispatched payload.
50
+ * Underlies both `context.with.always` and `With.Always`.
51
+ *
52
+ * @internal
53
+ */
54
+ export declare function makeAlways(key: string, value: unknown): (context: HandlerContext<Model, Actions, Props, Env>) => void;
55
+ /**
56
+ * Builds the {@link WithHandle} object returned via `context.with`. The
57
+ * runtime is identical for any model &mdash; only the call-site types differ.
58
+ *
59
+ * @internal
60
+ */
61
+ export declare function bindWith<M extends Model | void>(): WithHandle<M>;
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "march-hare",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "packageManager": "yarn@1.22.22",
7
7
  "main": "./dist/march-hare.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "bin": {
10
+ "mh": "./dist/cli/bin/mh.js"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "types": "./dist/index.d.ts",
@@ -14,8 +17,16 @@
14
17
  }
15
18
  },
16
19
  "dependencies": {
20
+ "@inquirer/prompts": "^7.2.0",
21
+ "change-case": "^5.4.4",
22
+ "ejs": "^3.1.10",
17
23
  "eventemitter3": "^5.0.4",
18
- "immertation": "^0.1.26"
24
+ "figlet": "^1.7.0",
25
+ "find-up": "^8.0.0",
26
+ "gray-matter": "^4.0.3",
27
+ "immertation": "^0.1.26",
28
+ "kleur": "^4.1.5",
29
+ "tinyglobby": "^0.2.17"
19
30
  },
20
31
  "peerDependencies": {
21
32
  "@mobily/ts-belt": "^3.0.0",
@@ -26,10 +37,12 @@
26
37
  "vitest/vite": "^7.3.1"
27
38
  },
28
39
  "files": [
29
- "dist"
40
+ "dist",
41
+ "src/cli/README.md"
30
42
  ],
31
43
  "scripts": {
32
- "build": "vite build",
44
+ "build": "vite build && npm run build:cli",
45
+ "build:cli": "tsc -p tsconfig.cli.json && rm -rf dist/cli/templates && cp -R src/cli/templates dist/cli/templates && chmod +x dist/cli/bin/mh.js",
33
46
  "build:example": "vite build --mode example --outDir dist-example --base /MarchHare/ && mv dist-example/src/example/index.html dist-example/index.html && rm -rf dist-example/src",
34
47
  "dev": "vite",
35
48
  "preview": "vite preview",
@@ -44,6 +57,7 @@
44
57
  "@emotion/css": "^11.13.5",
45
58
  "@eslint/js": "^9.39.3",
46
59
  "@faker-js/faker": "^10.3.0",
60
+ "@feature-sliced/eslint-config": "^0.1.1",
47
61
  "@jest/globals": "^30.2.0",
48
62
  "@js-temporal/polyfill": "^0.5.1",
49
63
  "@mobily/ts-belt": "4.0.0-rc.5",
@@ -52,6 +66,7 @@
52
66
  "@testing-library/jest-dom": "^6.9.1",
53
67
  "@testing-library/react": "^16.3.2",
54
68
  "@types/dom-navigation": "^1.0.7",
69
+ "@types/ejs": "^3.1.5",
55
70
  "@types/lodash": "^4.17.23",
56
71
  "@types/ms": "^2.1.0",
57
72
  "@types/ramda": "^0.31.1",
@@ -64,6 +79,8 @@
64
79
  "dayjs": "^1.11.19",
65
80
  "dexie": "^4.3.0",
66
81
  "eslint": "^9.39.3",
82
+ "eslint-import-resolver-typescript": "^4.4.5",
83
+ "eslint-plugin-boundaries": "^6.0.2",
67
84
  "eslint-plugin-fp": "^2.3.0",
68
85
  "eslint-plugin-import": "^2.32.0",
69
86
  "eslint-plugin-react": "^7.37.5",