march-hare 0.13.0 → 0.13.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -1
- package/dist/action/index.d.ts +2 -2
- package/dist/action/utils.d.ts +2 -2
- package/dist/actions/index.d.ts +2 -2
- package/dist/actions/types.d.ts +3 -3
- package/dist/actions/utils.d.ts +3 -3
- package/dist/app/index.d.ts +7 -7
- package/dist/app/types.d.ts +5 -5
- package/dist/boundary/components/broadcast/index.d.ts +2 -2
- package/dist/boundary/components/broadcast/types.d.ts +1 -1
- package/dist/boundary/components/consumer/components/partition/index.d.ts +1 -1
- package/dist/boundary/components/consumer/components/partition/types.d.ts +1 -1
- package/dist/boundary/components/consumer/index.d.ts +5 -5
- package/dist/boundary/components/consumer/types.d.ts +1 -1
- package/dist/boundary/components/consumer/utils.d.ts +1 -1
- package/dist/boundary/components/env/index.d.ts +3 -27
- package/dist/boundary/components/env/types.d.ts +24 -2
- package/dist/boundary/components/env/utils.d.ts +1 -1
- package/dist/boundary/components/scope/index.d.ts +2 -2
- package/dist/boundary/components/scope/types.d.ts +1 -1
- package/dist/boundary/components/scope/utils.d.ts +1 -1
- package/dist/boundary/components/sharing/index.d.ts +1 -1
- package/dist/boundary/components/tap/index.d.ts +3 -3
- package/dist/boundary/components/tap/types.d.ts +2 -2
- package/dist/boundary/components/tap/utils.d.ts +1 -1
- package/dist/boundary/components/tasks/index.d.ts +2 -2
- package/dist/boundary/components/tasks/utils.d.ts +1 -1
- package/dist/boundary/index.d.ts +1 -1
- package/dist/boundary/types.d.ts +2 -2
- package/dist/cache/index.d.ts +91 -9
- package/dist/cache/types.d.ts +1 -1
- package/dist/cli/bin/mh.js +10 -0
- package/dist/cli/lib/banner/index.js +14 -0
- package/dist/cli/lib/commands/app/index.js +37 -0
- package/dist/cli/lib/commands/feature/index.js +55 -0
- package/dist/cli/lib/commands/index.js +89 -0
- package/dist/cli/lib/commands/init/index.js +29 -0
- package/dist/cli/lib/commands/shared/index.js +56 -0
- package/dist/cli/lib/index.js +56 -0
- package/dist/cli/lib/parser/index.js +24 -0
- package/dist/cli/lib/prompt/index.js +61 -0
- package/dist/cli/lib/runner/index.js +46 -0
- package/dist/cli/lib/runner/types.js +1 -0
- package/dist/cli/lib/runner/utils.js +60 -0
- package/dist/cli/lib/types.js +1 -0
- package/dist/cli/lib/utils.js +20 -0
- package/dist/cli/templates/app/action/actions.ts.ejs.t +10 -0
- package/dist/cli/templates/app/action/types.ts.ejs.t +7 -0
- package/dist/cli/templates/app/integration/index.integration.tsx.ejs.t +13 -0
- package/dist/cli/templates/app/page/actions.ts.ejs.t +14 -0
- package/dist/cli/templates/app/page/index.tsx.ejs.t +20 -0
- package/dist/cli/templates/app/page/styles.ts.ejs.t +35 -0
- package/dist/cli/templates/app/page/types.ts.ejs.t +12 -0
- package/dist/cli/templates/feature/action/actions.ts.ejs.t +10 -0
- package/dist/cli/templates/feature/action/types.ts.ejs.t +7 -0
- package/dist/cli/templates/feature/multicast/types.ts.ejs.t +7 -0
- package/dist/cli/templates/feature/presentational/index.tsx.ejs.t +14 -0
- package/dist/cli/templates/feature/presentational/types.ts.ejs.t +12 -0
- package/dist/cli/templates/feature/presentational/utils.ts.ejs.t +8 -0
- package/dist/cli/templates/feature/stateful/actions.ts.ejs.t +16 -0
- package/dist/cli/templates/feature/stateful/index.tsx.ejs.t +19 -0
- package/dist/cli/templates/feature/stateful/types.ts.ejs.t +16 -0
- package/dist/cli/templates/feature/stateful/utils.ts.ejs.t +8 -0
- package/dist/cli/templates/feature/unit/index.test.tsx.ejs.t +21 -0
- package/dist/cli/templates/init/new/README.md.ejs.t +48 -0
- package/dist/cli/templates/init/new/eslint.config.js.ejs.t +88 -0
- package/dist/cli/templates/init/new/gitignore.ejs.t +9 -0
- package/dist/cli/templates/init/new/index.html.ejs.t +18 -0
- package/dist/cli/templates/init/new/package.json.ejs.t +54 -0
- package/dist/cli/templates/init/new/playwright.config.ts.ejs.t +17 -0
- package/dist/cli/templates/init/new/prettierrc.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.app.index.tsx.ejs.t +14 -0
- package/dist/cli/templates/init/new/src.app.pages.home.actions.ts.ejs.t +16 -0
- package/dist/cli/templates/init/new/src.app.pages.home.index.tsx.ejs.t +30 -0
- package/dist/cli/templates/init/new/src.app.pages.home.integration.tsx.ejs.t +28 -0
- package/dist/cli/templates/init/new/src.app.pages.home.styles.ts.ejs.t +45 -0
- package/dist/cli/templates/init/new/src.app.pages.home.types.ts.ejs.t +12 -0
- package/dist/cli/templates/init/new/src.app.utils.ts.ejs.t +9 -0
- package/dist/cli/templates/init/new/src.features.greet.actions.ts.ejs.t +20 -0
- package/dist/cli/templates/init/new/src.features.greet.index.test.tsx.ejs.t +21 -0
- package/dist/cli/templates/init/new/src.features.greet.index.tsx.ejs.t +24 -0
- package/dist/cli/templates/init/new/src.features.greet.types.ts.ejs.t +18 -0
- package/dist/cli/templates/init/new/src.features.greet.utils.ts.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.index.tsx.ejs.t +8 -0
- package/dist/cli/templates/init/new/src.shared.components.button.index.test.tsx.ejs.t +13 -0
- package/dist/cli/templates/init/new/src.shared.components.button.index.tsx.ejs.t +10 -0
- package/dist/cli/templates/init/new/src.shared.components.button.types.ts.ejs.t +6 -0
- package/dist/cli/templates/init/new/src.shared.resources.index.ts.ejs.t +4 -0
- package/dist/cli/templates/init/new/src.shared.theme.index.ts.ejs.t +51 -0
- package/dist/cli/templates/init/new/src.shared.types.index.ts.ejs.t +23 -0
- package/dist/cli/templates/init/new/src.test-setup.ts.ejs.t +10 -0
- package/dist/cli/templates/init/new/src.vite-env.d.ts.ejs.t +4 -0
- package/dist/cli/templates/init/new/tests.home.e2e.ts.ejs.t +14 -0
- package/dist/cli/templates/init/new/tsconfig.json.ejs.t +29 -0
- package/dist/cli/templates/init/new/vite.config.ts.ejs.t +17 -0
- package/dist/cli/templates/init/new/vitest.config.ts.ejs.t +24 -0
- package/dist/cli/templates/shared/component/index.tsx.ejs.t +9 -0
- package/dist/cli/templates/shared/component/types.ts.ejs.t +8 -0
- package/dist/cli/templates/shared/resource/index.ts.ejs.t +15 -0
- package/dist/cli/templates/shared/resource/types.ts.ejs.t +10 -0
- package/dist/cli/templates/shared/type-broadcast/types.ts.ejs.t +7 -0
- package/dist/cli/templates/shared/type-payload/types.ts.ejs.t +9 -0
- package/dist/cli/templates/shared/unit-component/index.test.tsx.ejs.t +13 -0
- package/dist/cli/templates/shared/unit-resource/index.test.ts.ejs.t +15 -0
- package/dist/cli/templates/shared/unit-util/index.test.ts.ejs.t +11 -0
- package/dist/cli/templates/shared/util/index.ts.ejs.t +6 -0
- package/dist/coalesce/index.d.ts +1 -1
- package/dist/context/index.d.ts +1 -1
- package/dist/error/types.d.ts +1 -1
- package/dist/error/utils.d.ts +1 -1
- package/dist/index.d.ts +17 -17
- package/dist/march-hare.js +9 -8
- package/dist/march-hare.js.map +1 -1
- package/dist/march-hare.umd.cjs +1 -1
- package/dist/march-hare.umd.cjs.map +1 -1
- package/dist/resource/index.d.ts +6 -5
- package/dist/resource/types.d.ts +3 -3
- package/dist/resource/utils.d.ts +12 -5
- package/dist/scope/index.d.ts +3 -3
- package/dist/scope/types.d.ts +2 -2
- package/dist/scope/utils.d.ts +6 -4
- package/dist/shared/index.d.ts +4 -4
- package/dist/types/index.d.ts +5 -5
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/types.d.ts +1 -1
- package/dist/utils/utils.d.ts +1 -1
- package/dist/with/index.d.ts +3 -3
- package/dist/with/types.d.ts +2 -2
- package/dist/with/utils.d.ts +3 -3
- package/package.json +18 -4
- package/src/cli/README.md +314 -0
package/README.md
CHANGED
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
1. [Multicast actions](#multicast-actions)
|
|
28
28
|
1. [Global data](#global-data)
|
|
29
29
|
1. [Reusable components](#reusable-components)
|
|
30
|
+
1. [Scaffolding CLI](#scaffolding-cli)
|
|
30
31
|
|
|
31
|
-
For advanced topics, see the [recipes directory](./recipes/). For a worked end-to-end example with the FSD layout, see [`src/example/`](./src/example/README.md).
|
|
32
|
+
For advanced topics, see the [recipes directory](./recipes/). For a worked end-to-end example with the FSD layout, see [`src/example/`](./src/example/README.md). To scaffold a new project that mirrors that example, see [`src/cli/`](./src/cli/README.md).
|
|
32
33
|
|
|
33
34
|
## Benefits
|
|
34
35
|
|
|
@@ -606,6 +607,26 @@ export default function CatCard(): React.ReactElement {
|
|
|
606
607
|
|
|
607
608
|
`Cache()` with no adapter is an in-memory scope – useful in tests or when you want a holdable cache without persistence. Per-params keying via `JSON.stringify(params)` is automatic, so `user({ id: 5 })` and `user({ id: 6 })` are distinct slots.
|
|
608
609
|
|
|
610
|
+
For multi-tenant apps that share a single backing store, add a `key(context)` callback alongside the adapter methods to derive a per-context prefix from the live `<app.Boundary>` Env. The callback receives the same `{ env }` an `app.Resource` fetcher sees; its return value is prepended to every cache slot, so two users on the same device do not see each other's data:
|
|
611
|
+
|
|
612
|
+
```ts
|
|
613
|
+
// app.ts
|
|
614
|
+
type AppEnv = { session: { accessToken: string } | null };
|
|
615
|
+
|
|
616
|
+
export const app = App<AppEnv>({
|
|
617
|
+
env: { session: null },
|
|
618
|
+
cache: Cache<AppEnv>({
|
|
619
|
+
get: (key) => localStorage.getItem(key),
|
|
620
|
+
set: (key, value) => localStorage.setItem(key, value),
|
|
621
|
+
remove: (key) => localStorage.removeItem(key),
|
|
622
|
+
clear: () => localStorage.clear(),
|
|
623
|
+
key: ({ env }) => env.session?.accessToken ?? "",
|
|
624
|
+
}),
|
|
625
|
+
});
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Successful writes for Alice land under `alice:0:{...}`; Bob's land under `bob:0:{...}`. Return `""`, `null`, or `undefined` to skip prefixing – useful for the signed-out gap, where the scope is genuinely empty.
|
|
629
|
+
|
|
609
630
|
The adapter contract is **strictly synchronous** – `get` / `set` / `remove` / `clear` all return immediately, with no `Promise`. The model-literal read (`{ user: resource.user.get() }`) is evaluated during render and has no place to wait. React Native projects should use [`react-native-mmkv`](https://github.com/mrousavy/react-native-mmkv), which is sync out of the box and drops straight into the contract; `AsyncStorage` is incompatible. Truly async backends (IndexedDB, `chrome.storage.local`) need a sync facade hydrated at app entry – see the [storage recipe](./recipes/storage.md).
|
|
610
631
|
|
|
611
632
|
See the [storage recipe](./recipes/storage.md) for backend adapters (React Native `react-native-mmkv`, browser `localStorage`, browser extension `chrome.storage`), sign-out purge, and the `unset` sentinel that keeps "nothing stored" distinct from "a legitimately stored null".
|
|
@@ -896,3 +917,26 @@ function Where(): React.ReactElement {
|
|
|
896
917
|
When a reusable component or resource is genuinely Env-agnostic — the fetcher never touches `context.env`, the hook never calls `shared.useEnv` — pass `Envless` as `E` instead of spelling out `Record<never, never>`: `shared.Resource<Envless, T>`, `shared.useContext<Envless, M, A>()`. It's a named alias for the empty-record shape exported from `march-hare`, kept around purely for legibility at the call site.
|
|
897
918
|
|
|
898
919
|
For one-line handler binding — flipping a boolean, assigning a payload to a leaf, pinning a field to a fixed value — reach for `context.with.{update,invert,always}`. See the [`With` helpers recipe](./recipes/with-helpers.md) for the full surface.
|
|
920
|
+
|
|
921
|
+
## Scaffolding CLI
|
|
922
|
+
|
|
923
|
+
A [Hygen](https://github.com/jondot/hygen)-style scaffolder ships under [`src/cli/`](./src/cli/) as the `mh` binary. It mirrors the layout of [`src/example/`](./src/example/) and the FSD layering rules enforced by `eslint-plugin-boundaries` — imports flow strictly downward (`app → features → shared`).
|
|
924
|
+
|
|
925
|
+
```bash
|
|
926
|
+
cd src/cli
|
|
927
|
+
npm install
|
|
928
|
+
npm link # creates the global `mh` binary
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
Run it with no arguments for an interactive menu, or drive any leaf command directly:
|
|
932
|
+
|
|
933
|
+
```bash
|
|
934
|
+
mh # banner + interactive menu
|
|
935
|
+
mh init my-project # bootstrap a new project
|
|
936
|
+
mh feature new add-cat # add a stateful feature
|
|
937
|
+
mh app new dashboard # add a page
|
|
938
|
+
mh shared component card # add a shared component
|
|
939
|
+
mh feature action counter Reset # inject an Action + handler stub
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Every command lives in a tree — typing `mh feature` opens a sub-menu, typing `mh feature new` prompts for a name, typing `mh feature new add-cat` runs non-interactively. See [`src/cli/README.md`](./src/cli/README.md) for the full command surface, the template format, and instructions for adding your own generators.
|
package/dist/action/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { HandlerPayload, BroadcastPayload, MulticastPayload, Distribution, Filter } from '../types/index';
|
|
2
|
-
export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils';
|
|
1
|
+
import { HandlerPayload, BroadcastPayload, MulticastPayload, Distribution, Filter } from '../types/index.js';
|
|
2
|
+
export { getActionSymbol, getLifecycleType, isBroadcastAction, isMulticastAction, getName, isChanneledAction, } from './utils.js';
|
|
3
3
|
/**
|
|
4
4
|
* Interface for the Action factory function.
|
|
5
5
|
*/
|
package/dist/action/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChanneledAction, AnyAction } from '../types/index';
|
|
2
|
-
import { ActionId } from '../boundary/components/tasks/types';
|
|
1
|
+
import { ChanneledAction, AnyAction } from '../types/index.js';
|
|
2
|
+
import { ActionId } from '../boundary/components/tasks/types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Extracts the underlying symbol from an action or channeled action.
|
|
5
5
|
* This symbol is used as the event emitter key for dispatching.
|
package/dist/actions/index.d.ts
CHANGED
package/dist/actions/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { default as EventEmitter } from 'eventemitter3';
|
|
2
2
|
import { RefObject } from 'react';
|
|
3
|
-
import { Model, HandlerContext, Actions, Props, Tasks, ActionId, Phase, Filter } from '../types/index';
|
|
4
|
-
import { BroadcastEmitter } from '../boundary/components/broadcast/utils';
|
|
5
|
-
import { ScopeContext } from '../boundary/components/scope/types';
|
|
3
|
+
import { Model, HandlerContext, Actions, Props, Tasks, ActionId, Phase, Filter } from '../types/index.js';
|
|
4
|
+
import { BroadcastEmitter } from '../boundary/components/broadcast/utils.js';
|
|
5
|
+
import { ScopeContext } from '../boundary/components/scope/types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Function signature for action handlers registered via `useAction`.
|
|
8
8
|
* Receives the reactive context and payload, returning void or a promise/generator.
|
package/dist/actions/utils.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { RefObject } from 'react';
|
|
2
|
-
import { Props, Model, Actions, Filter, ActionId, HandlerPayload, ChanneledAction, HandlerContext } from '../types/index';
|
|
2
|
+
import { Props, Model, Actions, Filter, ActionId, HandlerPayload, ChanneledAction, HandlerContext } from '../types/index.js';
|
|
3
3
|
import { default as EventEmitter } from 'eventemitter3';
|
|
4
|
-
import { Dispatchers, LifecycleConfig, Scope } from './types';
|
|
5
|
-
import { isChanneledAction, getActionSymbol } from '../action/index';
|
|
4
|
+
import { Dispatchers, LifecycleConfig, Scope } from './types.js';
|
|
5
|
+
import { isChanneledAction, getActionSymbol } from '../action/index.js';
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
/**
|
|
8
8
|
* Creates a new object with getters for each property of the input object.
|
package/dist/app/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { Env } from '../boundary/components/env/
|
|
2
|
-
import { Cache } from '../cache/index';
|
|
3
|
-
import { Actions, Model, Props } from '../types/index';
|
|
4
|
-
import { AppHandle, AppContextHandle } from './types';
|
|
5
|
-
import { Tap } from '../boundary/components/tap/types';
|
|
6
|
-
export type { AppArgs, AppContextHandle, AppFetcher, AppResource, } from './types';
|
|
7
|
-
export type { AppHandle } from './types';
|
|
1
|
+
import { Env } from '../boundary/components/env/types.js';
|
|
2
|
+
import { Cache } from '../cache/index.js';
|
|
3
|
+
import { Actions, Model, Props } from '../types/index.js';
|
|
4
|
+
import { AppHandle, AppContextHandle } from './types.js';
|
|
5
|
+
import { Tap } from '../boundary/components/tap/types.js';
|
|
6
|
+
export type { AppArgs, AppContextHandle, AppFetcher, AppResource, } from './types.js';
|
|
7
|
+
export type { AppHandle } from './types.js';
|
|
8
8
|
/**
|
|
9
9
|
* Creates an `App` — the entrypoint for a typed Env shape `E`,
|
|
10
10
|
* inferred from `config.env`. `App<E>` exposes `Boundary`, hooks, and
|
package/dist/app/types.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { Args, ResourceHandle } from '../resource/types';
|
|
2
|
-
import { Actions, Context, Model, Props, UseActions } from '../types/index';
|
|
3
|
-
import { Data } from '../actions/types';
|
|
4
|
-
import { Env } from '../boundary/components/env/
|
|
5
|
-
import { WithHandle } from '../with/types';
|
|
1
|
+
import { Args, ResourceHandle } from '../resource/types.js';
|
|
2
|
+
import { Actions, Context, Model, Props, UseActions } from '../types/index.js';
|
|
3
|
+
import { Data } from '../actions/types.js';
|
|
4
|
+
import { Env } from '../boundary/components/env/types.js';
|
|
5
|
+
import { WithHandle } from '../with/types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Args object passed to an `app.Resource` fetcher. Same shape as the
|
|
8
8
|
* base `Resource` fetcher's args but with `env` typed as `E`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useBroadcast, BroadcastEmitter } from './utils';
|
|
3
|
+
export { useBroadcast, BroadcastEmitter } from './utils.js';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a new broadcast context for distributed actions. Only needed if you
|
|
6
6
|
* want to isolate a broadcast context, useful for libraries that want to provide
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useConsumer } from './utils';
|
|
4
|
-
export { Partition } from './components/partition/index';
|
|
5
|
-
export type { Props as PartitionProps } from './components/partition/types';
|
|
6
|
-
export type { ConsumerRenderer, Entry, ConsumerContext } from './types';
|
|
3
|
+
export { useConsumer } from './utils.js';
|
|
4
|
+
export { Partition } from './components/partition/index.js';
|
|
5
|
+
export type { Props as PartitionProps } from './components/partition/types.js';
|
|
6
|
+
export type { ConsumerRenderer, Entry, ConsumerContext } from './types.js';
|
|
7
7
|
/**
|
|
8
8
|
* Creates a new consumer context for storing distributed action values. Only needed if you
|
|
9
9
|
* want to isolate a consumer context, useful for libraries that want to provide
|
|
@@ -1,32 +1,8 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useEnv } from './utils';
|
|
3
|
+
export { useEnv } from './utils.js';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
* narrows this to its own typed env via `App<E>({ env })`; the
|
|
7
|
-
* loose type exists so the framework's internal plumbing
|
|
8
|
-
* (`<Boundary>`, `useEnv`, handler `context.env`, Resource
|
|
9
|
-
* fetcher `context.env`) does not need to be parametric over E.
|
|
10
|
-
*
|
|
11
|
-
* Consumers should declare their Env shape inline via `App({ env })`
|
|
12
|
-
* — the inferred `E` is what flows through `app.useContext`,
|
|
13
|
-
* `app.useEnv`, and `app.Resource`. Module augmentation of `Env`
|
|
14
|
-
* is no longer required.
|
|
15
|
-
*/
|
|
16
|
-
export type Env = Record<string, unknown>;
|
|
17
|
-
/**
|
|
18
|
-
* `E` generic for `shared.X<E, ...>` factories whose callers don't read
|
|
19
|
-
* anything off the Env. Equivalent to `Record<never, never>` — the
|
|
20
|
-
* named alias keeps consumer sites legible (`shared.Resource<Envless, T>`
|
|
21
|
-
* over `shared.Resource<Record<never, never>, T>`) and signals intent.
|
|
22
|
-
*
|
|
23
|
-
* Reach for `Envless` only when the component or resource is genuinely
|
|
24
|
-
* Env-agnostic. Anything that reads `context.env.x` should declare the
|
|
25
|
-
* required shape (or a union of host Envs) as `E` instead.
|
|
26
|
-
*/
|
|
27
|
-
export type Envless = Record<never, never>;
|
|
28
|
-
/**
|
|
29
|
-
* Provides a per-Boundary {@link Env} value to every component inside
|
|
5
|
+
* Provides a per-Boundary {@link EnvType} value to every component inside
|
|
30
6
|
* the boundary. Usually wired in via the `<Boundary env={initial}>`
|
|
31
7
|
* prop rather than used directly.
|
|
32
8
|
*
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import { ReactNode } from 'react';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Loose runtime shape for the per-`<Boundary>` Env. Each {@link App}
|
|
4
|
+
* narrows this to its own typed env via `App<E>({ env })`; the
|
|
5
|
+
* loose type exists so the framework's internal plumbing
|
|
6
|
+
* (`<Boundary>`, `useEnv`, handler `context.env`, Resource
|
|
7
|
+
* fetcher `context.env`) does not need to be parametric over E.
|
|
8
|
+
*
|
|
9
|
+
* Consumers should declare their Env shape inline via `App({ env })`
|
|
10
|
+
* — the inferred `E` is what flows through `app.useContext`,
|
|
11
|
+
* `app.useEnv`, and `app.Resource`. Module augmentation of `Env`
|
|
12
|
+
* is no longer required.
|
|
13
|
+
*/
|
|
14
|
+
export type Env = Record<string, unknown>;
|
|
15
|
+
/**
|
|
16
|
+
* `E` generic for `shared.X<E, ...>` factories whose callers don't read
|
|
17
|
+
* anything off the Env. Equivalent to `Record<never, never>` — the
|
|
18
|
+
* named alias keeps consumer sites legible (`shared.Resource<Envless, T>`
|
|
19
|
+
* over `shared.Resource<Record<never, never>, T>`) and signals intent.
|
|
20
|
+
*
|
|
21
|
+
* Reach for `Envless` only when the component or resource is genuinely
|
|
22
|
+
* Env-agnostic. Anything that reads `context.env.x` should declare the
|
|
23
|
+
* required shape (or a union of host Envs) as `E` instead.
|
|
24
|
+
*/
|
|
25
|
+
export type Envless = Record<never, never>;
|
|
4
26
|
/**
|
|
5
27
|
* Props for the Env provider component. Accepts the initial Env
|
|
6
28
|
* value that satisfies the augmented {@link Env} interface.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { Context, useScope, getScope } from './utils';
|
|
2
|
-
export type { ScopeEntry, ScopeContext } from './types';
|
|
1
|
+
export { Context, useScope, getScope } from './utils.js';
|
|
2
|
+
export type { ScopeEntry, ScopeContext } from './types.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export { useTap } from './utils';
|
|
4
|
-
export type { Tap, Taps, Invocation, Failure, Mutations, Snapshot, } from './types';
|
|
3
|
+
export { useTap } from './utils.js';
|
|
4
|
+
export type { Tap, Taps, Invocation, Failure, Mutations, Snapshot, } from './types.js';
|
|
5
5
|
/**
|
|
6
6
|
* Internal provider that wires a {@link Tap} observer into the React
|
|
7
7
|
* context consumed by `useActions` during dispatch. Rendered by the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Reason } from '../../../error/types';
|
|
2
|
-
import { Task } from '../tasks/types';
|
|
1
|
+
import { Reason } from '../../../error/types.js';
|
|
2
|
+
import { Task } from '../tasks/types.js';
|
|
3
3
|
import type * as React from "react";
|
|
4
4
|
/**
|
|
5
5
|
* Identity of a handler invocation: the action being handled and the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
export type { Task } from './types';
|
|
3
|
+
export type { Task } from './types.js';
|
|
4
4
|
/**
|
|
5
5
|
* Creates a new tasks context for action control. Only needed if you
|
|
6
6
|
* want to isolate a tasks context, useful for libraries that want to provide
|
package/dist/boundary/index.d.ts
CHANGED
package/dist/boundary/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Env } from './components/env/types';
|
|
2
|
-
import { Tap } from './components/tap/types';
|
|
1
|
+
import { Env } from './components/env/types.js';
|
|
2
|
+
import { Tap } from './components/tap/types.js';
|
|
3
3
|
import type * as React from "react";
|
|
4
4
|
/**
|
|
5
5
|
* Props accepted by the bare `<Boundary>` provider.
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
|
-
import { Adapter, Stored } from './types';
|
|
2
|
-
|
|
1
|
+
import { Adapter, Stored } from './types.js';
|
|
2
|
+
import { Env } from '../boundary/components/env/types.js';
|
|
3
|
+
export type { Adapter, Encoded } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Context passed to {@link CacheConfig.key}. Mirrors the shape an
|
|
6
|
+
* `app.Resource` fetcher receives, restricted to the field the cache
|
|
7
|
+
* needs to scope on: the live per-`<Boundary>` Env. Future-extensible
|
|
8
|
+
* — new fields can land here without breaking the call shape.
|
|
9
|
+
*
|
|
10
|
+
* @template E The Env shape the cache is parameterised by.
|
|
11
|
+
*/
|
|
12
|
+
export type CacheContext<E extends object> = {
|
|
13
|
+
readonly env: E;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Configuration accepted by the {@link Cache} factory. Combines the
|
|
17
|
+
* synchronous {@link Adapter} (`get`/`set`/`remove`/`clear`/`keys?`)
|
|
18
|
+
* with an optional `key(context)` callback in a single flat object,
|
|
19
|
+
* so all the cache's plumbing lives in one literal at the call site.
|
|
20
|
+
*
|
|
21
|
+
* - `key` — derives a per-context cache scope. Called every time
|
|
22
|
+
* a cache key is assembled with the same `{ env }` shape an
|
|
23
|
+
* `app.Resource` fetcher receives; the returned string is prepended
|
|
24
|
+
* to the per-resource namespace and params so different scopes
|
|
25
|
+
* (e.g. one cache slot per access token, locale, or tenant id) can
|
|
26
|
+
* coexist in the same backing store. Return `""`, `null`, or
|
|
27
|
+
* `undefined` to skip prefixing — useful for "not signed in"
|
|
28
|
+
* gaps where the scope is genuinely empty.
|
|
29
|
+
*/
|
|
30
|
+
export type CacheConfig<E extends object> = Adapter & {
|
|
31
|
+
readonly key?: (context: CacheContext<E>) => string | null | undefined;
|
|
32
|
+
};
|
|
3
33
|
/**
|
|
4
34
|
* Persistence-aware cache for a single {@link Resource}. Wraps a
|
|
5
35
|
* **strictly synchronous** {@link Adapter} (localStorage, MMKV,
|
|
@@ -25,6 +55,21 @@ export type { Adapter, Encoded } from './types';
|
|
|
25
55
|
* persistent store; when supplied, the adapter is the **only** tier
|
|
26
56
|
* — the Cache does not maintain a separate in-memory mirror.
|
|
27
57
|
*
|
|
58
|
+
* Pass `key(context)` alongside the adapter methods to scope cache
|
|
59
|
+
* slots by the per-`<Boundary>` Env. The returned string is prepended
|
|
60
|
+
* to every cache key the Resource layer assembles, so different
|
|
61
|
+
* tenants / sessions / locales share the adapter without stepping on
|
|
62
|
+
* each other.
|
|
63
|
+
*
|
|
64
|
+
* The `E` generic lives on the {@link Cache} factory and on
|
|
65
|
+
* {@link CacheConfig}: it parameterises the `key(context)` callback
|
|
66
|
+
* at construction time so the caller can read `context.env.X` with
|
|
67
|
+
* full typing. The returned {@link Cache} value is itself
|
|
68
|
+
* env-agnostic — the runtime `scope(env)` method takes the
|
|
69
|
+
* loose {@link Env} record and narrows internally before invoking
|
|
70
|
+
* the callback — which keeps it freely assignable across
|
|
71
|
+
* differently-typed Apps without variance gymnastics.
|
|
72
|
+
*
|
|
28
73
|
* @example
|
|
29
74
|
* ```ts
|
|
30
75
|
* // In-memory, scoped to this instance.
|
|
@@ -38,6 +83,16 @@ export type { Adapter, Encoded } from './types';
|
|
|
38
83
|
* clear: () => localStorage.clear(),
|
|
39
84
|
* });
|
|
40
85
|
*
|
|
86
|
+
* // Multi-tenant: writes go under `${accessToken}:…`.
|
|
87
|
+
* type AppEnv = { session: { accessToken: string } | null };
|
|
88
|
+
* const cache = Cache<AppEnv>({
|
|
89
|
+
* get: (key) => localStorage.getItem(key),
|
|
90
|
+
* set: (key, value) => localStorage.setItem(key, value),
|
|
91
|
+
* remove: (key) => localStorage.removeItem(key),
|
|
92
|
+
* clear: () => localStorage.clear(),
|
|
93
|
+
* key: ({ env }) => env.session?.accessToken ?? "",
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
41
96
|
* // Wire it into a Resource — successful runs write through automatically.
|
|
42
97
|
* export const cat = Resource({
|
|
43
98
|
* cache,
|
|
@@ -87,14 +142,41 @@ export type Cache = {
|
|
|
87
142
|
* stored params satisfy a `where` pattern.
|
|
88
143
|
*/
|
|
89
144
|
keys(): Iterable<string>;
|
|
145
|
+
/**
|
|
146
|
+
* Returns the per-context prefix derived from the configured
|
|
147
|
+
* `key(context)`. The returned string is appended with `:` by the
|
|
148
|
+
* Resource layer to compose the full cache key. Always `""` when
|
|
149
|
+
* no `key` option was supplied or when the callback returned an
|
|
150
|
+
* empty value — "no scope" is encoded as the empty string.
|
|
151
|
+
*
|
|
152
|
+
* Takes the loose {@link Env} record at runtime — the typed
|
|
153
|
+
* `E` lives on the `key(context)` callback registered at
|
|
154
|
+
* construction time, which the cache narrows to `E` internally
|
|
155
|
+
* before invoking.
|
|
156
|
+
*
|
|
157
|
+
* @internal Public surface lives on the Resource layer; consumers
|
|
158
|
+
* should not need to call this directly.
|
|
159
|
+
*/
|
|
160
|
+
scope(env: Env | undefined): string;
|
|
90
161
|
};
|
|
91
162
|
/**
|
|
92
|
-
* Constructs a {@link Cache}
|
|
93
|
-
*
|
|
94
|
-
*
|
|
163
|
+
* Constructs a {@link Cache} from `config`. The config object carries
|
|
164
|
+
* the synchronous adapter methods (`get`/`set`/`remove`/`clear`/`keys?`)
|
|
165
|
+
* and, optionally, a `key(context)` callback that scopes every cache
|
|
166
|
+
* slot by the live per-`<Boundary>` Env. Omit `config` entirely for an
|
|
167
|
+
* in-memory cache scoped to this instance.
|
|
168
|
+
*
|
|
169
|
+
* When `key` is supplied, it runs each time the Resource layer
|
|
170
|
+
* assembles a cache key, receiving the same `{ env }` shape an
|
|
171
|
+
* `app.Resource` fetcher sees; its return value is prepended
|
|
172
|
+
* (separated by `:`) to the per-resource namespace and params JSON.
|
|
95
173
|
*
|
|
96
|
-
* @
|
|
97
|
-
*
|
|
98
|
-
*
|
|
174
|
+
* @template E The Env shape `config.key` is typed against. Defaults
|
|
175
|
+
* to the loose {@link Env} record so callers that don't scope by
|
|
176
|
+
* env can keep using `Cache({ ...adapter })` without supplying a
|
|
177
|
+
* generic.
|
|
178
|
+
* @param config Optional adapter-plus-options literal. Omit for an
|
|
179
|
+
* in-memory cache; supply adapter methods alone for a persisted
|
|
180
|
+
* cache; add `key` to also scope writes by the live Env.
|
|
99
181
|
*/
|
|
100
|
-
export declare function Cache(
|
|
182
|
+
export declare function Cache<E extends object = Env>(config?: CacheConfig<E>): Cache;
|
package/dist/cache/types.d.ts
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { main } from "../lib/index.js";
|
|
4
|
+
main(process.argv.slice(2)).catch((error) => {
|
|
5
|
+
if (error instanceof Error && error.name === "ExitPromptError") {
|
|
6
|
+
process.exit(130);
|
|
7
|
+
}
|
|
8
|
+
console.error(error);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import figlet from "figlet";
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import { config } from "../utils.js";
|
|
4
|
+
export function banner() {
|
|
5
|
+
const art = figlet.textSync(config.banner.title, {
|
|
6
|
+
font: config.banner.font,
|
|
7
|
+
horizontalLayout: "default",
|
|
8
|
+
verticalLayout: "default",
|
|
9
|
+
});
|
|
10
|
+
console.log(kleur.magenta(art));
|
|
11
|
+
console.log(kleur.gray(` ${config.banner.tagline} `) +
|
|
12
|
+
kleur.dim(config.banner.subtitle));
|
|
13
|
+
console.log();
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import { pascalCase, capitalCase } from "change-case";
|
|
4
|
+
import { scaffold } from "../../runner/index.js";
|
|
5
|
+
import { askName, askDescription, pickDirectory, requireProjectRoot, } from "../../prompt/index.js";
|
|
6
|
+
export async function newPage({ positional, flags, }) {
|
|
7
|
+
const root = requireProjectRoot();
|
|
8
|
+
const name = positional[0] || (await askName("Page name (kebab-case)"));
|
|
9
|
+
const heading = (typeof flags.heading === "string" ? flags.heading : undefined) ||
|
|
10
|
+
capitalCase(name);
|
|
11
|
+
const tagline = (typeof flags.tagline === "string" ? flags.tagline : undefined) ||
|
|
12
|
+
(await askDescription("Page tagline", `Welcome to ${heading}`));
|
|
13
|
+
await scaffold("app", "page", { name, heading, tagline, pascalName: pascalCase(name) }, { cwd: root });
|
|
14
|
+
console.log(kleur.green("\n Page ready."), kleur.dim(`Wire it up in src/app/index.tsx with <${pascalCase(name)}Page />.`));
|
|
15
|
+
}
|
|
16
|
+
export async function integration({ positional }) {
|
|
17
|
+
const root = requireProjectRoot();
|
|
18
|
+
const pagesRoot = path.join(root, "src", "app", "pages");
|
|
19
|
+
const name = positional[0] || (await pickDirectory("page", pagesRoot));
|
|
20
|
+
await scaffold("app", "integration", { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
21
|
+
console.log(kleur.green("\n Integration test added."), kleur.dim(`Run with \`make integration\` or \`npx playwright test\`.`));
|
|
22
|
+
}
|
|
23
|
+
export async function action({ positional, flags, }) {
|
|
24
|
+
const root = requireProjectRoot();
|
|
25
|
+
const pagesRoot = path.join(root, "src", "app", "pages");
|
|
26
|
+
const page = positional[0] || (await pickDirectory("page", pagesRoot));
|
|
27
|
+
const name = positional[1] ||
|
|
28
|
+
(typeof flags.name === "string" ? flags.name : undefined) ||
|
|
29
|
+
(await askName("Action name (PascalCase)"));
|
|
30
|
+
await scaffold("app", "action", {
|
|
31
|
+
page,
|
|
32
|
+
name: pascalCase(name),
|
|
33
|
+
pascalName: pascalCase(name),
|
|
34
|
+
rawName: name,
|
|
35
|
+
}, { cwd: root });
|
|
36
|
+
console.log(kleur.green("\n Action added."), kleur.dim(`Dispatch with actions.dispatch(Actions.${pascalCase(name)}).`));
|
|
37
|
+
}
|