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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import type { Props } from "./types.ts";
|
|
6
|
+
import { scope } from "./utils.ts";
|
|
7
|
+
|
|
8
|
+
export function <%= pascalName %>({ label }: Props): React.ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<scope.Boundary>
|
|
11
|
+
<span>{label}</span>
|
|
12
|
+
</scope.Boundary>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/types.ts
|
|
3
|
+
---
|
|
4
|
+
import { Action, Distribution } from "march-hare";
|
|
5
|
+
|
|
6
|
+
export type Props = {
|
|
7
|
+
label: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class Multicast {
|
|
11
|
+
static Update = Action<string>("<%= pascalName %>.Update", Distribution.Multicast);
|
|
12
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/actions.ts
|
|
3
|
+
---
|
|
4
|
+
import { Actions, type Model } from "./types.ts";
|
|
5
|
+
import { scope } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
export function useActions() {
|
|
8
|
+
const context = scope.useContext<Model, typeof Actions>();
|
|
9
|
+
const actions = context.useActions({ count: 0 });
|
|
10
|
+
|
|
11
|
+
actions.useAction(Actions.Tick, (context) => {
|
|
12
|
+
context.actions.produce(({ model }) => void (model.count += 1));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return actions;
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { useActions } from "./actions.ts";
|
|
6
|
+
import { scope } from "./utils.ts";
|
|
7
|
+
import { Actions } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export function <%= pascalName %>(): React.ReactElement {
|
|
10
|
+
const [model, actions] = useActions();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<scope.Boundary>
|
|
14
|
+
<button onClick={() => actions.dispatch(Actions.Tick)}>
|
|
15
|
+
<%= pascalName %> ({model.count})
|
|
16
|
+
</button>
|
|
17
|
+
</scope.Boundary>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/types.ts
|
|
3
|
+
---
|
|
4
|
+
import { Action, Distribution } from "march-hare";
|
|
5
|
+
|
|
6
|
+
export type Model = {
|
|
7
|
+
count: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export class Actions {
|
|
11
|
+
static Tick = Action("<%= pascalName %>.Tick");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class Multicast {
|
|
15
|
+
static Update = Action<number>("<%= pascalName %>.Update", Distribution.Multicast);
|
|
16
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/<%= name %>/index.test.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render } from "@testing-library/react";
|
|
6
|
+
import { App } from "march-hare";
|
|
7
|
+
import { <%= pascalName %> } from "./index.tsx";
|
|
8
|
+
import { type Envs } from "@shared/types/index.ts";
|
|
9
|
+
|
|
10
|
+
const app = App<Envs>({ env: { apiBase: "https://api.example.test" } });
|
|
11
|
+
|
|
12
|
+
describe("<%= pascalName %>", () => {
|
|
13
|
+
it("renders inside an app boundary", () => {
|
|
14
|
+
render(
|
|
15
|
+
<app.Boundary>
|
|
16
|
+
<<%= pascalName %> />
|
|
17
|
+
</app.Boundary>,
|
|
18
|
+
);
|
|
19
|
+
expect(document.body).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: README.md
|
|
3
|
+
---
|
|
4
|
+
# <%= title(name) %>
|
|
5
|
+
|
|
6
|
+
<%= description %>
|
|
7
|
+
|
|
8
|
+
Built with [March Hare](https://github.com/Wildhoney/MarchHare) and the
|
|
9
|
+
[`@march-hare/cli`](https://github.com/Wildhoney/MarchHare/tree/main/src/cli)
|
|
10
|
+
scaffolder.
|
|
11
|
+
|
|
12
|
+
## Layout
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
src/
|
|
16
|
+
├── app/ ← the host. One App() declaration, one Boundary, all routes.
|
|
17
|
+
├── features/ ← user-facing behaviours. Each feature owns its own scope.
|
|
18
|
+
└── shared/ ← reusable building blocks: types, resources, theme, utils.
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Layer boundaries are enforced by `eslint-plugin-boundaries` — imports
|
|
22
|
+
flow strictly downward (`app → features → shared`).
|
|
23
|
+
|
|
24
|
+
## Scripts
|
|
25
|
+
|
|
26
|
+
| Command | What it does |
|
|
27
|
+
| -------------------- | ----------------------------------------- |
|
|
28
|
+
| `yarn dev` | Start the Vite dev server |
|
|
29
|
+
| `yarn test` | Run unit + integration tests with Vitest |
|
|
30
|
+
| `yarn test:e2e` | Run Playwright end-to-end tests |
|
|
31
|
+
| `yarn typecheck` | TypeScript with no emit |
|
|
32
|
+
| `yarn lint` | ESLint |
|
|
33
|
+
| `yarn checks` | Format, lint, typecheck, and test |
|
|
34
|
+
|
|
35
|
+
## Scaffolding more
|
|
36
|
+
|
|
37
|
+
With the CLI installed globally (`npm i -g @march-hare/cli`) or linked
|
|
38
|
+
locally, drop new pages, features, and shared modules in:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
mh # interactive menu
|
|
42
|
+
mh feature new # add a feature
|
|
43
|
+
mh app new # add a page
|
|
44
|
+
mh shared component # add a shared component
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
See the [CLI README](https://github.com/Wildhoney/MarchHare/tree/main/src/cli)
|
|
48
|
+
for the full list.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: eslint.config.js
|
|
3
|
+
---
|
|
4
|
+
import js from "@eslint/js";
|
|
5
|
+
import globals from "globals";
|
|
6
|
+
import tseslint from "typescript-eslint";
|
|
7
|
+
import pluginReact from "eslint-plugin-react";
|
|
8
|
+
import pluginImport from "eslint-plugin-import";
|
|
9
|
+
import pluginBoundaries from "eslint-plugin-boundaries";
|
|
10
|
+
import { defineConfig } from "eslint/config";
|
|
11
|
+
|
|
12
|
+
export default defineConfig([
|
|
13
|
+
{
|
|
14
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
15
|
+
plugins: { js },
|
|
16
|
+
extends: ["js/recommended"],
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
files: ["**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
|
|
20
|
+
languageOptions: { globals: globals.browser },
|
|
21
|
+
},
|
|
22
|
+
tseslint.configs.recommended,
|
|
23
|
+
pluginReact.configs.flat.recommended,
|
|
24
|
+
{
|
|
25
|
+
plugins: { import: pluginImport },
|
|
26
|
+
settings: {
|
|
27
|
+
react: { version: "detect" },
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
"react/react-in-jsx-scope": "off",
|
|
31
|
+
"@typescript-eslint/consistent-type-definitions": ["error", "type"],
|
|
32
|
+
"@typescript-eslint/no-non-null-assertion": "error",
|
|
33
|
+
"@typescript-eslint/no-namespace": "off",
|
|
34
|
+
"@typescript-eslint/no-unused-vars": [
|
|
35
|
+
"error",
|
|
36
|
+
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
|
|
37
|
+
],
|
|
38
|
+
"import/no-default-export": "error",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
files: ["src/**/*.{ts,tsx,d.ts}"],
|
|
43
|
+
plugins: { boundaries: pluginBoundaries },
|
|
44
|
+
settings: {
|
|
45
|
+
"import/resolver": {
|
|
46
|
+
typescript: { project: "./tsconfig.json" },
|
|
47
|
+
node: true,
|
|
48
|
+
},
|
|
49
|
+
"boundaries/include": ["src/**/*"],
|
|
50
|
+
"boundaries/elements": [
|
|
51
|
+
{ type: "app", pattern: "src/app", mode: "folder" },
|
|
52
|
+
{
|
|
53
|
+
type: "features",
|
|
54
|
+
pattern: "src/features/*",
|
|
55
|
+
mode: "folder",
|
|
56
|
+
capture: ["slice"],
|
|
57
|
+
},
|
|
58
|
+
{ type: "shared", pattern: "src/shared", mode: "folder" },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
rules: {
|
|
62
|
+
"import/no-default-export": "off",
|
|
63
|
+
"boundaries/dependencies": [
|
|
64
|
+
"error",
|
|
65
|
+
{
|
|
66
|
+
default: "disallow",
|
|
67
|
+
message:
|
|
68
|
+
"${file.type} cannot import ${dependency.type} — layering is top-down (app → features → shared).",
|
|
69
|
+
rules: [
|
|
70
|
+
{ from: { type: "app" }, allow: { to: { type: ["app", "features", "shared"] } } },
|
|
71
|
+
{ from: { type: "features" }, allow: { to: { type: "shared" } } },
|
|
72
|
+
{ from: { type: "shared" }, allow: { to: { type: "shared" } } },
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
files: ["**/*.test.{ts,tsx}", "**/*.integration.{ts,tsx}", "tests/**/*.{ts,tsx}"],
|
|
80
|
+
rules: {
|
|
81
|
+
"@typescript-eslint/no-floating-promises": "off",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
files: ["vite.config.ts", "vitest.config.ts", "playwright.config.ts", "eslint.config.js"],
|
|
86
|
+
rules: { "import/no-default-export": "off" },
|
|
87
|
+
},
|
|
88
|
+
]);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: index.html
|
|
3
|
+
---
|
|
4
|
+
<!doctype html>
|
|
5
|
+
<html lang="en">
|
|
6
|
+
<head>
|
|
7
|
+
<meta charset="UTF-8" />
|
|
8
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
9
|
+
<title><%= title(name) %></title>
|
|
10
|
+
<style>
|
|
11
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
12
|
+
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #f5f5f5; }
|
|
13
|
+
</style>
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<script type="module" src="/src/index.tsx"></script>
|
|
17
|
+
</body>
|
|
18
|
+
</html>
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: package.json
|
|
3
|
+
---
|
|
4
|
+
{
|
|
5
|
+
"name": "<%= name %>",
|
|
6
|
+
"version": "0.1.0",
|
|
7
|
+
"description": "<%= description %>",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"private": true,
|
|
10
|
+
"scripts": {
|
|
11
|
+
"dev": "vite",
|
|
12
|
+
"build": "vite build",
|
|
13
|
+
"preview": "vite preview",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"test:e2e": "playwright test",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"lint": "eslint src/",
|
|
19
|
+
"fmt": "prettier --write .",
|
|
20
|
+
"checks": "npm run fmt && npm run lint && npm run typecheck && npm run test"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@emotion/css": "^11.13.5",
|
|
24
|
+
"@mobily/ts-belt": "^3.0.0",
|
|
25
|
+
"antd": "^6.3.0",
|
|
26
|
+
"immer": "^10.0.0",
|
|
27
|
+
"ky": "^2.0.2",
|
|
28
|
+
"march-hare": "^0.13.0",
|
|
29
|
+
"react": "^19.0.0",
|
|
30
|
+
"react-dom": "^19.0.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@eslint/js": "^9.39.3",
|
|
34
|
+
"@playwright/test": "^1.58.2",
|
|
35
|
+
"@testing-library/dom": "^10.4.1",
|
|
36
|
+
"@testing-library/jest-dom": "^6.9.1",
|
|
37
|
+
"@testing-library/react": "^16.3.2",
|
|
38
|
+
"@types/react": "^19.2.14",
|
|
39
|
+
"@types/react-dom": "^19.2.3",
|
|
40
|
+
"@vitejs/plugin-react": "^5.1.4",
|
|
41
|
+
"eslint": "^9.39.3",
|
|
42
|
+
"eslint-import-resolver-typescript": "^4.4.5",
|
|
43
|
+
"eslint-plugin-boundaries": "^6.0.2",
|
|
44
|
+
"eslint-plugin-import": "^2.32.0",
|
|
45
|
+
"eslint-plugin-react": "^7.37.5",
|
|
46
|
+
"globals": "^17.3.0",
|
|
47
|
+
"happy-dom": "^20.6.1",
|
|
48
|
+
"prettier": "^3.8.1",
|
|
49
|
+
"typescript": "^6.0.3",
|
|
50
|
+
"typescript-eslint": "^8.55.0",
|
|
51
|
+
"vite": "^7.3.1",
|
|
52
|
+
"vitest": "^4.0.18"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: playwright.config.ts
|
|
3
|
+
---
|
|
4
|
+
import { defineConfig } from "@playwright/test";
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
testDir: "./tests",
|
|
8
|
+
testMatch: "*.e2e.ts",
|
|
9
|
+
use: {
|
|
10
|
+
baseURL: "http://localhost:5173",
|
|
11
|
+
},
|
|
12
|
+
webServer: {
|
|
13
|
+
command: "vite dev",
|
|
14
|
+
port: 5173,
|
|
15
|
+
reuseExistingServer: !process.env.CI,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { app } from "./utils.ts";
|
|
6
|
+
import { HomePage } from "./pages/home/index.tsx";
|
|
7
|
+
|
|
8
|
+
export function Root(): React.ReactElement {
|
|
9
|
+
return (
|
|
10
|
+
<app.Boundary>
|
|
11
|
+
<HomePage />
|
|
12
|
+
</app.Boundary>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/actions.ts
|
|
3
|
+
---
|
|
4
|
+
import { app } from "../../utils.ts";
|
|
5
|
+
import { Actions, type Model } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export function useActions() {
|
|
8
|
+
const context = app.useContext<Model, typeof Actions>();
|
|
9
|
+
const actions = context.useActions({ greeting: null });
|
|
10
|
+
|
|
11
|
+
actions.useAction(Actions.Broadcast.Greeted, (context, message) => {
|
|
12
|
+
context.actions.produce(({ model }) => void (model.greeting = message));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return actions;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { GreetButton } from "@features/greet/index.tsx";
|
|
6
|
+
import { useActions } from "./actions.ts";
|
|
7
|
+
import * as styles from "./styles.ts";
|
|
8
|
+
|
|
9
|
+
export function HomePage(): React.ReactElement {
|
|
10
|
+
const [model] = useActions();
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<main className={styles.layout}>
|
|
14
|
+
<header className={styles.header}>
|
|
15
|
+
<h1 className={styles.title}><%= title(name) %></h1>
|
|
16
|
+
<p className={styles.tagline}>
|
|
17
|
+
Click the button below to dispatch your first March Hare action.
|
|
18
|
+
</p>
|
|
19
|
+
</header>
|
|
20
|
+
|
|
21
|
+
<GreetButton />
|
|
22
|
+
|
|
23
|
+
{model.greeting ? (
|
|
24
|
+
<p className={styles.greeting}>{model.greeting}</p>
|
|
25
|
+
) : (
|
|
26
|
+
<p className={styles.empty}>No greeting yet — press the button.</p>
|
|
27
|
+
)}
|
|
28
|
+
</main>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/index.integration.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen, act, fireEvent, waitFor } from "@testing-library/react";
|
|
6
|
+
import { Root } from "@app/index.tsx";
|
|
7
|
+
|
|
8
|
+
describe("HomePage", () => {
|
|
9
|
+
it("renders the heading", () => {
|
|
10
|
+
render(<Root />);
|
|
11
|
+
expect(screen.getByRole("heading", { name: "<%= title(name) %>" })).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("shows the empty state until the button is clicked", () => {
|
|
15
|
+
render(<Root />);
|
|
16
|
+
expect(screen.getByText(/No greeting yet/)).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("broadcasts a greeting on click", async () => {
|
|
20
|
+
render(<Root />);
|
|
21
|
+
await act(async () => {
|
|
22
|
+
fireEvent.click(screen.getByRole("button", { name: /Say hello/ }));
|
|
23
|
+
});
|
|
24
|
+
await waitFor(() => {
|
|
25
|
+
expect(screen.queryByText(/No greeting yet/)).not.toBeInTheDocument();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/app/pages/home/styles.ts
|
|
3
|
+
---
|
|
4
|
+
import { css } from "@emotion/css";
|
|
5
|
+
import { colour, font, spacing } from "@shared/theme/index.ts";
|
|
6
|
+
|
|
7
|
+
export const layout = css`
|
|
8
|
+
min-height: 100vh;
|
|
9
|
+
padding: ${spacing.xxl} ${spacing.l};
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: column;
|
|
12
|
+
align-items: center;
|
|
13
|
+
gap: ${spacing.xl};
|
|
14
|
+
font-family: ${font.family};
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
export const header = css`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: column;
|
|
20
|
+
align-items: center;
|
|
21
|
+
gap: ${spacing.xs};
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export const title = css`
|
|
25
|
+
margin: 0;
|
|
26
|
+
font-size: ${font.size.xxl};
|
|
27
|
+
font-weight: ${font.weight.bold};
|
|
28
|
+
color: ${colour.text.primary};
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const tagline = css`
|
|
32
|
+
margin: 0;
|
|
33
|
+
color: ${colour.text.secondary};
|
|
34
|
+
font-size: ${font.size.m};
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
export const greeting = css`
|
|
38
|
+
font-size: ${font.size.l};
|
|
39
|
+
color: ${colour.text.primary};
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
export const empty = css`
|
|
43
|
+
color: ${colour.text.muted};
|
|
44
|
+
font-style: italic;
|
|
45
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/actions.ts
|
|
3
|
+
---
|
|
4
|
+
import { Actions, type Model } from "./types.ts";
|
|
5
|
+
import { scope } from "./utils.ts";
|
|
6
|
+
|
|
7
|
+
export function useActions() {
|
|
8
|
+
const context = scope.useContext<Model, typeof Actions>();
|
|
9
|
+
const actions = context.useActions({ count: 0 });
|
|
10
|
+
|
|
11
|
+
actions.useAction(Actions.Click, async (context) => {
|
|
12
|
+
context.actions.produce(({ model }) => void (model.count += 1));
|
|
13
|
+
await context.actions.dispatch(
|
|
14
|
+
Actions.Broadcast.Greeted,
|
|
15
|
+
`Hello from <%= title(name) %> #${context.model.count + 1}`,
|
|
16
|
+
);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return actions;
|
|
20
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/index.test.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
import { App } from "march-hare";
|
|
7
|
+
import { GreetButton } from "./index.tsx";
|
|
8
|
+
import { type Envs } from "@shared/types/index.ts";
|
|
9
|
+
|
|
10
|
+
const app = App<Envs>({ env: { apiBase: "https://api.example.test" } });
|
|
11
|
+
|
|
12
|
+
describe("GreetButton", () => {
|
|
13
|
+
it("renders the Say hello button", () => {
|
|
14
|
+
render(
|
|
15
|
+
<app.Boundary>
|
|
16
|
+
<GreetButton />
|
|
17
|
+
</app.Boundary>,
|
|
18
|
+
);
|
|
19
|
+
expect(screen.getByRole("button", { name: "Say hello" })).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/index.tsx
|
|
3
|
+
---
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { Button } from "@shared/components/button/index.tsx";
|
|
6
|
+
import { useActions } from "./actions.ts";
|
|
7
|
+
import { scope } from "./utils.ts";
|
|
8
|
+
import { Actions } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export function GreetButton(): React.ReactElement {
|
|
11
|
+
const [, actions] = useActions();
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<scope.Boundary>
|
|
15
|
+
<Button
|
|
16
|
+
type="primary"
|
|
17
|
+
size="large"
|
|
18
|
+
onClick={() => actions.dispatch(Actions.Click)}
|
|
19
|
+
>
|
|
20
|
+
Say hello
|
|
21
|
+
</Button>
|
|
22
|
+
</scope.Boundary>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/features/greet/types.ts
|
|
3
|
+
---
|
|
4
|
+
import { Action, Distribution } from "march-hare";
|
|
5
|
+
import { Broadcast } from "@shared/types/index.ts";
|
|
6
|
+
|
|
7
|
+
export type Model = {
|
|
8
|
+
count: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Actions {
|
|
12
|
+
static Click = Action("Greet.Click");
|
|
13
|
+
static Broadcast = Broadcast;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class Multicast {
|
|
17
|
+
static Pulse = Action<number>("Greet.Pulse", Distribution.Multicast);
|
|
18
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
to: src/shared/components/button/index.test.tsx
|
|
3
|
+
---
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { render, screen } from "@testing-library/react";
|
|
6
|
+
import { Button } from "./index.tsx";
|
|
7
|
+
|
|
8
|
+
describe("Button", () => {
|
|
9
|
+
it("renders its children", () => {
|
|
10
|
+
render(<Button>Click me</Button>);
|
|
11
|
+
expect(screen.getByRole("button", { name: "Click me" })).toBeInTheDocument();
|
|
12
|
+
});
|
|
13
|
+
});
|