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.
- package/README.md +66 -25
- 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 +33 -87
- package/dist/app/types.d.ts +79 -26
- 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 -16
- 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 +3 -3
- 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 +3 -3
- package/dist/boundary/types.d.ts +3 -3
- package/dist/cache/index.d.ts +68 -12
- package/dist/cache/types.d.ts +33 -19
- 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 +2 -2
- package/dist/error/index.d.ts +18 -1
- package/dist/error/types.d.ts +1 -18
- package/dist/error/utils.d.ts +1 -1
- package/dist/index.d.ts +16 -14
- package/dist/march-hare.js +7 -6
- package/dist/march-hare.js.map +1 -0
- package/dist/march-hare.umd.cjs +2 -1
- package/dist/march-hare.umd.cjs.map +1 -0
- package/dist/resource/index.d.ts +32 -61
- package/dist/resource/types.d.ts +45 -22
- package/dist/resource/utils.d.ts +31 -3
- package/dist/scope/index.d.ts +4 -64
- package/dist/scope/types.d.ts +8 -8
- package/dist/scope/utils.d.ts +12 -0
- package/dist/shared/index.d.ts +12 -21
- package/dist/types/index.d.ts +114 -29
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/types.d.ts +1 -3
- package/dist/utils/utils.d.ts +1 -3
- package/dist/with/index.d.ts +17 -62
- package/dist/with/types.d.ts +66 -0
- package/dist/with/utils.d.ts +61 -0
- package/package.json +21 -4
- package/src/cli/README.md +314 -0
|
@@ -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,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Invocation } from '../../../resource/index.js';
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Per-`<Boundary>` registry for `.coalesce(token)` sharing. Outer map
|
|
5
|
-
* keys on the `
|
|
5
|
+
* keys on the `Invocation.run` function identity (stable per Resource
|
|
6
6
|
* via the `build()` closure); inner map keys on
|
|
7
7
|
* `${paramsKey}|${coalesceKey(token)}`. While an entry exists every
|
|
8
8
|
* caller awaiting `.coalesce(token)` for the same Resource + params +
|
|
@@ -14,7 +14,7 @@ import * as React from "react";
|
|
|
14
14
|
*
|
|
15
15
|
* @internal
|
|
16
16
|
*/
|
|
17
|
-
export type Sharing = WeakMap<
|
|
17
|
+
export type Sharing = WeakMap<Invocation<unknown, object>["run"], Map<string, Promise<unknown>>>;
|
|
18
18
|
/**
|
|
19
19
|
* React context exposing the per-Boundary sharing registry. The
|
|
20
20
|
* fallback is a fresh `WeakMap` used when `useSharing()` is read
|
|
@@ -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
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { Props } from './types';
|
|
1
|
+
import { Props } from './types.js';
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
/**
|
|
4
4
|
* Low-level boundary primitive. Wraps children with the Broadcaster,
|
|
5
5
|
* Env, and Tasks providers required by every March Hare hook.
|
|
6
6
|
*
|
|
7
7
|
* Most applications should reach for {@link App} instead —
|
|
8
|
-
* `App<
|
|
8
|
+
* `App<E>({ env })` returns a typed `app.Boundary` along with
|
|
9
9
|
* matching `useContext` / `useEnv` / `Resource` factories that all
|
|
10
|
-
* close over the App's inferred env shape `
|
|
10
|
+
* close over the App's inferred env shape `E`. The bare `Boundary`
|
|
11
11
|
* is exposed for advanced or library-internal use where the loose
|
|
12
12
|
* Env record type is sufficient.
|
|
13
13
|
*
|
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.
|
|
@@ -18,7 +18,7 @@ import type * as React from "react";
|
|
|
18
18
|
export type Props = {
|
|
19
19
|
/**
|
|
20
20
|
* Initial value of the per-Boundary {@link Env}. Prefer `App({ env })`
|
|
21
|
-
* — it infers the Env shape `
|
|
21
|
+
* — it infers the Env shape `E` and threads it through
|
|
22
22
|
* `app.useContext`, `app.useEnv`, and `app.Resource`, so handler
|
|
23
23
|
* `context.env` is typed accordingly. Pass `env` directly here only
|
|
24
24
|
* for advanced cases where the loose record type is sufficient.
|
package/dist/cache/index.d.ts
CHANGED
|
@@ -1,19 +1,29 @@
|
|
|
1
|
-
import { Adapter, Stored } from './types';
|
|
2
|
-
export type { Adapter, Encoded } from './types';
|
|
1
|
+
import { Adapter, Stored } from './types.js';
|
|
2
|
+
export type { Adapter, Encoded } from './types.js';
|
|
3
3
|
/**
|
|
4
4
|
* Persistence-aware cache for a single {@link Resource}. Wraps a
|
|
5
|
-
* synchronous {@link Adapter} (localStorage, MMKV,
|
|
6
|
-
* sync facade, etc.) and traffics in {@link
|
|
7
|
-
* storage entries serialise as {@link
|
|
8
|
-
* `Temporal.Instant` timestamp survives the
|
|
9
|
-
* `.exceeds({...})` can short-circuit on the
|
|
10
|
-
* a reload.
|
|
5
|
+
* **strictly synchronous** {@link Adapter} (localStorage, MMKV,
|
|
6
|
+
* chrome.storage with a sync facade, etc.) and traffics in {@link
|
|
7
|
+
* Stored} envelopes — storage entries serialise as {@link
|
|
8
|
+
* Encoded}`<T>` so the `Temporal.Instant` timestamp survives the
|
|
9
|
+
* string round-trip and `.exceeds({...})` can short-circuit on the
|
|
10
|
+
* persisted timestamp after a reload.
|
|
11
|
+
*
|
|
12
|
+
* Every method on the Cache is sync — the model-literal sync
|
|
13
|
+
* read has no place to wait, so the adapter contract foregoes
|
|
14
|
+
* `Promise` entirely. Async backends (IndexedDB, AsyncStorage,
|
|
15
|
+
* `chrome.storage.local`) need a sync facade hydrated at app entry;
|
|
16
|
+
* see `recipes/storage.md` for the pattern. React Native projects
|
|
17
|
+
* should reach for {@link https://github.com/mrousavy/react-native-mmkv
|
|
18
|
+
* `react-native-mmkv`} — it's synchronous out of the box and
|
|
19
|
+
* drops straight into the Adapter contract.
|
|
11
20
|
*
|
|
12
21
|
* Call with no arguments for an in-memory cache scoped to this
|
|
13
|
-
* instance — useful for tests, ephemeral state, or when you
|
|
14
|
-
* first-class cache object to share between Resources without
|
|
22
|
+
* instance — useful for tests, ephemeral state, or when you
|
|
23
|
+
* want a first-class cache object to share between Resources without
|
|
15
24
|
* persistence. Pass an {@link Adapter} to back the cache with a
|
|
16
|
-
* persistent store
|
|
25
|
+
* persistent store; when supplied, the adapter is the **only** tier
|
|
26
|
+
* — the Cache does not maintain a separate in-memory mirror.
|
|
17
27
|
*
|
|
18
28
|
* @example
|
|
19
29
|
* ```ts
|
|
@@ -36,9 +46,55 @@ export type { Adapter, Encoded } from './types';
|
|
|
36
46
|
* ```
|
|
37
47
|
*/
|
|
38
48
|
export type Cache = {
|
|
49
|
+
/**
|
|
50
|
+
* Returns the {@link Stored} envelope for `key`. The envelope is
|
|
51
|
+
* `empty()` when nothing is persisted; otherwise it carries the
|
|
52
|
+
* decoded payload and the timestamp recorded at write-time.
|
|
53
|
+
*
|
|
54
|
+
* @template T The payload type expected at `key`.
|
|
55
|
+
* @param key Cache slot identifier — usually the JSON-stringified
|
|
56
|
+
* call-site params, prefixed by the Resource's namespace.
|
|
57
|
+
*/
|
|
39
58
|
get<T>(key: string): Stored<T>;
|
|
40
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Writes `value` to `key`. Skipped when the envelope has no concrete
|
|
61
|
+
* payload (e.g. an `empty()` slot), since there is nothing meaningful
|
|
62
|
+
* to persist. Serialisation, quota errors, and unserialisable payloads
|
|
63
|
+
* are swallowed — writes are best-effort.
|
|
64
|
+
*
|
|
65
|
+
* @template T The payload type contained in `value`.
|
|
66
|
+
* @param key Cache slot identifier — usually the JSON-stringified
|
|
67
|
+
* call-site params, prefixed by the Resource's namespace.
|
|
68
|
+
* @param value Stored envelope carrying the payload and its
|
|
69
|
+
* write-time `Temporal.Instant`.
|
|
70
|
+
*/
|
|
71
|
+
set<T>(key: string, value: Stored<T>): void;
|
|
72
|
+
/**
|
|
73
|
+
* Drops a single cache slot. Best-effort — backing-store errors
|
|
74
|
+
* are swallowed.
|
|
75
|
+
*
|
|
76
|
+
* @param key Cache slot identifier.
|
|
77
|
+
*/
|
|
41
78
|
remove(key: string): void;
|
|
79
|
+
/**
|
|
80
|
+
* Drops every cache slot in the backing store. Best-effort —
|
|
81
|
+
* backing-store errors are swallowed.
|
|
82
|
+
*/
|
|
42
83
|
clear(): void;
|
|
84
|
+
/**
|
|
85
|
+
* Returns every key currently held by the backing store. Used by
|
|
86
|
+
* partial-match eviction (`evict(where)`) to iterate slots whose
|
|
87
|
+
* stored params satisfy a `where` pattern.
|
|
88
|
+
*/
|
|
89
|
+
keys(): Iterable<string>;
|
|
43
90
|
};
|
|
91
|
+
/**
|
|
92
|
+
* Constructs a {@link Cache} backed by `adapter`, or by an in-memory
|
|
93
|
+
* `Map` when none is supplied. The returned object is the same shape
|
|
94
|
+
* regardless — only the durability differs.
|
|
95
|
+
*
|
|
96
|
+
* @param adapter Optional synchronous backing store (localStorage, MMKV,
|
|
97
|
+
* or a custom sync facade). Omit for an in-memory cache scoped to
|
|
98
|
+
* this instance.
|
|
99
|
+
*/
|
|
44
100
|
export declare function Cache(adapter?: Adapter): Cache;
|
package/dist/cache/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type { Stored } from '../utils/types';
|
|
1
|
+
export type { Stored } from '../utils/types.js';
|
|
2
2
|
/**
|
|
3
3
|
* On-disk JSON shape of a `Stored` envelope. The Cache wrapper
|
|
4
4
|
* encodes a populated Stored as `{ data, at: at.toString() }` so the
|
|
@@ -18,37 +18,51 @@ export type Encoded<T> = {
|
|
|
18
18
|
* facade, etc.) and pass to {@link Cache}. The adapter shuttles raw
|
|
19
19
|
* strings; JSON encoding and `Temporal.Instant` round-tripping happen
|
|
20
20
|
* inside the Cache wrapper, so adapters stay trivial.
|
|
21
|
+
*
|
|
22
|
+
* **Every method is strictly synchronous.** The library never awaits
|
|
23
|
+
* adapter calls — the model-literal sync read has no place to
|
|
24
|
+
* wait. Async backends (IndexedDB, AsyncStorage, chrome.storage.local)
|
|
25
|
+
* need a sync facade hydrated at app entry; see `recipes/storage.md`
|
|
26
|
+
* for the pattern. React Native projects should use
|
|
27
|
+
* {@link https://github.com/mrousavy/react-native-mmkv `react-native-mmkv`}
|
|
28
|
+
* — it's sync out of the box and drops straight into this
|
|
29
|
+
* contract.
|
|
21
30
|
*/
|
|
22
31
|
export type Adapter = {
|
|
23
32
|
/**
|
|
24
33
|
* Return the raw string stored under `key`, or `null` when no entry
|
|
25
|
-
* exists.
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `null` — the Cache falls through to its next fallback rather
|
|
29
|
-
* than crashing the render.
|
|
34
|
+
* exists. **Strictly sync.** Treat any read-time error (decryption,
|
|
35
|
+
* IPC, etc.) as "not found" and return `null` — the Cache will
|
|
36
|
+
* fall back to its empty state.
|
|
30
37
|
*/
|
|
31
38
|
readonly get: (key: string) => string | null;
|
|
32
39
|
/**
|
|
33
|
-
* Persist the raw string `value` under `key`.
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* swallows so a write failure can't poison an already-resolved fetch.
|
|
40
|
+
* Persist the raw string `value` under `key`. **Strictly sync.**
|
|
41
|
+
* Throwing is fine on quota, private mode, sandboxed iframes, etc.;
|
|
42
|
+
* the Cache catches and swallows so a failure can't poison an
|
|
43
|
+
* already-resolved fetch.
|
|
38
44
|
*/
|
|
39
45
|
readonly set: (key: string, value: string) => void;
|
|
40
46
|
/**
|
|
41
|
-
* Drop the entry at `key`. Idempotent —
|
|
42
|
-
* key that isn't present must not throw.
|
|
47
|
+
* Drop the entry at `key`. **Strictly sync.** Idempotent —
|
|
48
|
+
* calling `remove` for a key that isn't present must not throw.
|
|
43
49
|
*/
|
|
44
50
|
readonly remove: (key: string) => void;
|
|
45
51
|
/**
|
|
46
|
-
* Wipe every entry this adapter can see. On a
|
|
47
|
-
* `localStorage` this means the whole origin
|
|
48
|
-
* state, dismissed banners, route hints, etc.
|
|
49
|
-
* authors should either delegate to the
|
|
50
|
-
* (accepting that scope) or namespace by key
|
|
51
|
-
* their own.
|
|
52
|
+
* Wipe every entry this adapter can see. **Strictly sync.** On a
|
|
53
|
+
* shared backend such as `localStorage` this means the whole origin
|
|
54
|
+
* — third-party SDK state, dismissed banners, route hints, etc.
|
|
55
|
+
* all go with it. Adapter authors should either delegate to the
|
|
56
|
+
* backend's native clear (accepting that scope) or namespace by key
|
|
57
|
+
* prefix and remove only their own.
|
|
52
58
|
*/
|
|
53
59
|
readonly clear: () => void;
|
|
60
|
+
/**
|
|
61
|
+
* Optional enumerator over every key the adapter currently knows
|
|
62
|
+
* about. **Strictly sync** when implemented — partial-match
|
|
63
|
+
* evictions sweep these keys in the current tick. `localStorage`
|
|
64
|
+
* exposes this via `Object.keys(localStorage)`; MMKV via
|
|
65
|
+
* `getAllKeys()`.
|
|
66
|
+
*/
|
|
67
|
+
readonly keys?: () => Iterable<string>;
|
|
54
68
|
};
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import kleur from "kleur";
|
|
3
|
+
import { pascalCase } from "change-case";
|
|
4
|
+
import { scaffold } from "../../runner/index.js";
|
|
5
|
+
import { askName, askConfirm, pickDirectory, requireProjectRoot, } from "../../prompt/index.js";
|
|
6
|
+
async function resolveStateful(flags) {
|
|
7
|
+
if (flags.stateful !== undefined)
|
|
8
|
+
return flags.stateful !== false;
|
|
9
|
+
if (flags.presentational !== undefined)
|
|
10
|
+
return flags.presentational === false;
|
|
11
|
+
return askConfirm("Does this feature own state and actions?", true);
|
|
12
|
+
}
|
|
13
|
+
export async function newFeature({ positional, flags, }) {
|
|
14
|
+
const root = requireProjectRoot();
|
|
15
|
+
const name = positional[0] || (await askName("Feature name (kebab-case)"));
|
|
16
|
+
const stateful = await resolveStateful(flags);
|
|
17
|
+
const action = stateful ? "stateful" : "presentational";
|
|
18
|
+
await scaffold("feature", action, { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
19
|
+
console.log(kleur.green("\n Feature ready."), kleur.dim(`Mount it inside a page with <${pascalCase(name)} />.`));
|
|
20
|
+
}
|
|
21
|
+
export async function unit({ positional }) {
|
|
22
|
+
const root = requireProjectRoot();
|
|
23
|
+
const featuresRoot = path.join(root, "src", "features");
|
|
24
|
+
const name = positional[0] || (await pickDirectory("feature", featuresRoot));
|
|
25
|
+
await scaffold("feature", "unit", { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
26
|
+
}
|
|
27
|
+
export async function action({ positional, flags, }) {
|
|
28
|
+
const root = requireProjectRoot();
|
|
29
|
+
const featuresRoot = path.join(root, "src", "features");
|
|
30
|
+
const feature = positional[0] || (await pickDirectory("feature", featuresRoot));
|
|
31
|
+
const name = positional[1] ||
|
|
32
|
+
(typeof flags.name === "string" ? flags.name : undefined) ||
|
|
33
|
+
(await askName("Action name (PascalCase)"));
|
|
34
|
+
await scaffold("feature", "action", {
|
|
35
|
+
feature,
|
|
36
|
+
name: pascalCase(name),
|
|
37
|
+
pascalName: pascalCase(name),
|
|
38
|
+
rawName: name,
|
|
39
|
+
}, { cwd: root });
|
|
40
|
+
}
|
|
41
|
+
export async function multicast({ positional, flags, }) {
|
|
42
|
+
const root = requireProjectRoot();
|
|
43
|
+
const featuresRoot = path.join(root, "src", "features");
|
|
44
|
+
const feature = positional[0] || (await pickDirectory("feature", featuresRoot));
|
|
45
|
+
const name = positional[1] ||
|
|
46
|
+
(typeof flags.name === "string" ? flags.name : undefined) ||
|
|
47
|
+
(await askName("Multicast action (PascalCase)"));
|
|
48
|
+
await scaffold("feature", "multicast", {
|
|
49
|
+
feature,
|
|
50
|
+
featurePascal: pascalCase(feature),
|
|
51
|
+
name: pascalCase(name),
|
|
52
|
+
pascalName: pascalCase(name),
|
|
53
|
+
rawName: name,
|
|
54
|
+
}, { cwd: root });
|
|
55
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import * as init from "./init/index.js";
|
|
2
|
+
import * as app from "./app/index.js";
|
|
3
|
+
import * as feature from "./feature/index.js";
|
|
4
|
+
import * as shared from "./shared/index.js";
|
|
5
|
+
export const tree = {
|
|
6
|
+
init: {
|
|
7
|
+
leaf: true,
|
|
8
|
+
description: "Bootstrap a new March Hare project",
|
|
9
|
+
run: init.run,
|
|
10
|
+
},
|
|
11
|
+
app: {
|
|
12
|
+
leaf: false,
|
|
13
|
+
description: "Manage the host (pages, integration tests, actions)",
|
|
14
|
+
children: {
|
|
15
|
+
new: {
|
|
16
|
+
leaf: true,
|
|
17
|
+
description: "Create a new page under app/pages/",
|
|
18
|
+
run: app.newPage,
|
|
19
|
+
},
|
|
20
|
+
integration: {
|
|
21
|
+
leaf: true,
|
|
22
|
+
description: "Add an integration test for an existing page",
|
|
23
|
+
run: app.integration,
|
|
24
|
+
},
|
|
25
|
+
action: {
|
|
26
|
+
leaf: true,
|
|
27
|
+
description: "Add a new action handler to an existing page",
|
|
28
|
+
run: app.action,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
feature: {
|
|
33
|
+
leaf: false,
|
|
34
|
+
description: "Manage features (slices, unit tests, actions)",
|
|
35
|
+
children: {
|
|
36
|
+
new: {
|
|
37
|
+
leaf: true,
|
|
38
|
+
description: "Create a new feature slice",
|
|
39
|
+
run: feature.newFeature,
|
|
40
|
+
},
|
|
41
|
+
unit: {
|
|
42
|
+
leaf: true,
|
|
43
|
+
description: "Add a unit test next to an existing feature",
|
|
44
|
+
run: feature.unit,
|
|
45
|
+
},
|
|
46
|
+
action: {
|
|
47
|
+
leaf: true,
|
|
48
|
+
description: "Add a new action handler to an existing feature",
|
|
49
|
+
run: feature.action,
|
|
50
|
+
},
|
|
51
|
+
multicast: {
|
|
52
|
+
leaf: true,
|
|
53
|
+
description: "Add a multicast action to an existing feature's Scope",
|
|
54
|
+
run: feature.multicast,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
shared: {
|
|
59
|
+
leaf: false,
|
|
60
|
+
description: "Manage shared building blocks",
|
|
61
|
+
children: {
|
|
62
|
+
component: {
|
|
63
|
+
leaf: true,
|
|
64
|
+
description: "Create a new shared component",
|
|
65
|
+
run: shared.component,
|
|
66
|
+
},
|
|
67
|
+
resource: {
|
|
68
|
+
leaf: true,
|
|
69
|
+
description: "Create a new shared resource",
|
|
70
|
+
run: shared.resource,
|
|
71
|
+
},
|
|
72
|
+
util: {
|
|
73
|
+
leaf: true,
|
|
74
|
+
description: "Create a new shared utility",
|
|
75
|
+
run: shared.util,
|
|
76
|
+
},
|
|
77
|
+
type: {
|
|
78
|
+
leaf: true,
|
|
79
|
+
description: "Add a shared type/payload/broadcast namespace",
|
|
80
|
+
run: shared.type,
|
|
81
|
+
},
|
|
82
|
+
unit: {
|
|
83
|
+
leaf: true,
|
|
84
|
+
description: "Add a unit test next to an existing shared module",
|
|
85
|
+
run: shared.unit,
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
import { pascalCase } from "change-case";
|
|
5
|
+
import { scaffold } from "../../runner/index.js";
|
|
6
|
+
import { askName, askDescription } from "../../prompt/index.js";
|
|
7
|
+
export async function run({ positional, flags }) {
|
|
8
|
+
const rawName = positional[0] ||
|
|
9
|
+
(typeof flags.name === "string" ? flags.name : undefined) ||
|
|
10
|
+
(await askName("Project name", "my-app"));
|
|
11
|
+
const description = (typeof flags.description === "string" ? flags.description : undefined) ||
|
|
12
|
+
(await askDescription("Short description", `A March Hare project: ${rawName}`));
|
|
13
|
+
const apiBase = (typeof flags.apiBase === "string" ? flags.apiBase : undefined) ||
|
|
14
|
+
(await askDescription("Default API base URL", "https://api.example.com"));
|
|
15
|
+
const cwd = path.resolve(process.cwd(), rawName);
|
|
16
|
+
const env = pascalCase(rawName);
|
|
17
|
+
console.log();
|
|
18
|
+
console.log(kleur.bold(` Scaffolding ${kleur.magenta(rawName)} into ${kleur.gray(cwd)}`));
|
|
19
|
+
console.log();
|
|
20
|
+
await scaffold("init", "new", { name: rawName, description, apiBase, env }, { cwd });
|
|
21
|
+
console.log();
|
|
22
|
+
console.log(kleur.green(" Project ready."));
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(kleur.bold(" Next steps:"));
|
|
25
|
+
console.log(` cd ${rawName}`);
|
|
26
|
+
console.log(" yarn install");
|
|
27
|
+
console.log(" yarn dev");
|
|
28
|
+
console.log();
|
|
29
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import kleur from "kleur";
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import { pascalCase } from "change-case";
|
|
6
|
+
import { scaffold } from "../../runner/index.js";
|
|
7
|
+
import { askName, pickDirectory, requireProjectRoot, } from "../../prompt/index.js";
|
|
8
|
+
const sharedSubdirs = {
|
|
9
|
+
components: "component",
|
|
10
|
+
resources: "resource",
|
|
11
|
+
utils: "util",
|
|
12
|
+
};
|
|
13
|
+
export async function component({ positional }) {
|
|
14
|
+
const root = requireProjectRoot();
|
|
15
|
+
const name = positional[0] || (await askName("Component name (kebab-case)"));
|
|
16
|
+
await scaffold("shared", "component", { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
17
|
+
}
|
|
18
|
+
export async function resource({ positional }) {
|
|
19
|
+
const root = requireProjectRoot();
|
|
20
|
+
const name = positional[0] || (await askName("Resource name (kebab-case)"));
|
|
21
|
+
await scaffold("shared", "resource", { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
22
|
+
console.log(kleur.dim(`\n Remember to re-export from src/shared/resources/index.ts: export * as ${name.replace(/-/g, "")} from "./${name}/index.ts";`));
|
|
23
|
+
}
|
|
24
|
+
export async function util({ positional }) {
|
|
25
|
+
const root = requireProjectRoot();
|
|
26
|
+
const name = positional[0] || (await askName("Util name (kebab-case)"));
|
|
27
|
+
await scaffold("shared", "util", { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
28
|
+
}
|
|
29
|
+
export async function type({ positional, flags }) {
|
|
30
|
+
const root = requireProjectRoot();
|
|
31
|
+
const kind = positional[0] ||
|
|
32
|
+
(typeof flags.kind === "string" ? flags.kind : undefined) ||
|
|
33
|
+
(await select({
|
|
34
|
+
message: "Kind of type to add",
|
|
35
|
+
choices: [
|
|
36
|
+
{ name: "Payload — cross-feature data type", value: "payload" },
|
|
37
|
+
{ name: "Broadcast — global action class", value: "broadcast" },
|
|
38
|
+
],
|
|
39
|
+
}));
|
|
40
|
+
const name = positional[1] || (await askName(`${kind} name (kebab-case)`));
|
|
41
|
+
await scaffold("shared", `type-${kind}`, { name, pascalName: pascalCase(name) }, { cwd: root });
|
|
42
|
+
}
|
|
43
|
+
export async function unit({ positional }) {
|
|
44
|
+
const root = requireProjectRoot();
|
|
45
|
+
const sharedRoot = path.join(root, "src", "shared");
|
|
46
|
+
const kindKey = positional[0] ||
|
|
47
|
+
(await select({
|
|
48
|
+
message: "Which kind of shared module?",
|
|
49
|
+
choices: Object.keys(sharedSubdirs)
|
|
50
|
+
.filter((key) => fs.existsSync(path.join(sharedRoot, key)))
|
|
51
|
+
.map((key) => ({ name: key, value: key })),
|
|
52
|
+
}));
|
|
53
|
+
const dir = path.join(sharedRoot, kindKey);
|
|
54
|
+
const name = positional[1] || (await pickDirectory(kindKey, dir));
|
|
55
|
+
await scaffold("shared", `unit-${sharedSubdirs[kindKey]}`, { name, pascalName: pascalCase(name), kind: kindKey }, { cwd: root });
|
|
56
|
+
}
|