@pplancq/react-template 2.8.17 → 2.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/_README.md +57 -52
- package/mise.toml +2 -2
- package/package.json +25 -24
- package/scripts/removeDemo.cjs +105 -66
- package/src/README.md +81 -0
- package/src/app/App.tsx +18 -0
- package/src/{bootstrap.tsx → app/AppHTMLElement.tsx} +2 -2
- package/src/app/README.md +69 -0
- package/src/{__tests__/bootstraps.test.tsx → app/__tests__/AppHTMLElement.test.tsx} +5 -5
- package/src/app/config/serviceContainer.ts +6 -0
- package/src/app/providers/Providers/Providers.tsx +18 -0
- package/src/{providers → app/providers}/QueryClientProvider/QueryClientProvider.tsx +3 -3
- package/src/app/providers/ServiceProvider/ServiceContext.ts +8 -0
- package/src/app/providers/ServiceProvider/ServiceProvider.tsx +14 -0
- package/src/app/routing/Router.tsx +14 -0
- package/src/app/routing/appRoutes.ts +3 -0
- package/src/app/routing/routes.tsx +9 -0
- package/src/demo/README.md +148 -0
- package/src/demo/application/dtos/ArchitecturePrinciple.ts +5 -0
- package/src/{types/profileTypes.ts → demo/application/dtos/ProfileFormValues.ts} +1 -1
- package/src/demo/application/use-cases/GetArchitecturePrinciplesUseCase.ts +11 -0
- package/src/demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface.ts +5 -0
- package/src/demo/application/use-cases/GetDemoUsersUseCase.ts +11 -0
- package/src/demo/application/use-cases/GetDemoUsersUseCaseInterface.ts +5 -0
- package/src/demo/domain/entities/DemoUser.ts +7 -0
- package/src/demo/domain/repositories/ArchitecturePrincipleRepositoryInterface.d.ts +5 -0
- package/src/demo/domain/repositories/DemoUserRepositoryInterface.d.ts +5 -0
- package/src/demo/infrastructure/repositories/HttpDemoUserRepository.ts +29 -0
- package/src/demo/infrastructure/repositories/StaticArchitecturePrincipleRepository.ts +33 -0
- package/src/demo/serviceDemo.ts +39 -0
- package/src/demo/serviceIdentifiers.ts +6 -0
- package/src/demo/ui/components/DemoFooter/DemoFooter.tsx +17 -0
- package/src/{components/Footer → demo/ui/components/DemoFooter}/__tests__/Footer.test.tsx +2 -2
- package/src/{components/Footer → demo/ui/components/DemoFooter}/__tests__/__snapshots__/Footer.test.tsx.snap +3 -3
- package/src/demo/ui/components/DemoFooter/index.ts +1 -0
- package/src/demo/ui/components/DemoHeader/DemoHeader.tsx +25 -0
- package/src/{components/Header → demo/ui/components/DemoHeader}/__tests__/Header.test.tsx +2 -2
- package/src/demo/ui/components/DemoHeader/__tests__/__snapshots__/Header.test.tsx.snap +64 -0
- package/src/demo/ui/components/DemoHeader/index.ts +1 -0
- package/src/{ui/molecules/Navigation/Navigation.tsx → demo/ui/components/DemoNavigation/DemoNavigation.tsx} +4 -4
- package/src/demo/ui/components/DemoNavigation/index.ts +1 -0
- package/src/{components → demo/ui/components}/TextInput/TextInput.tsx +3 -3
- package/src/{forms → demo/ui/forms}/ProfileForm/ProfileForm.tsx +7 -7
- package/src/demo/ui/hooks/useDemoUsersQuery/useDemoUsersQuery.ts +13 -0
- package/src/demo/ui/layouts/DemoLayout/DemoLayout.tsx +12 -0
- package/src/demo/ui/layouts/MainTemplate/MainTemplate.module.css +21 -0
- package/src/{ui/templates → demo/ui/layouts}/MainTemplate/MainTemplate.tsx +7 -7
- package/src/demo/ui/pages/DemoHome/DemoHome.tsx +117 -0
- package/src/demo/ui/pages/DependencyInjectionDemo/DependencyInjectionDemo.tsx +47 -0
- package/src/{pages/Demo → demo/ui/pages}/ReactHookFormDemo/ReactHookFormDemo.tsx +7 -7
- package/src/demo/ui/pages/ReactQueryDemo/ReactQueryDemo.module.scss +38 -0
- package/src/{pages/Demo → demo/ui/pages}/ReactQueryDemo/ReactQueryDemo.tsx +5 -4
- package/src/demo/ui/routing/demoAppRoutes.ts +6 -0
- package/src/demo/ui/routing/demoRoutes.tsx +31 -0
- package/src/main.ts +2 -2
- package/src/shared/README.md +53 -0
- package/src/shared/application/README.md +49 -0
- package/src/shared/domain/README.md +52 -0
- package/src/shared/infrastructure/README.md +51 -0
- package/src/{api → shared/infrastructure/http}/fetchApi.ts +1 -1
- package/src/shared/ui/README.md +50 -0
- package/src/shared/ui/hooks/useService/useService.ts +13 -0
- package/tsconfig.json +3 -1
- package/vitest.config.mts +5 -1
- package/src/api/README.md +0 -65
- package/src/api/demoApi.ts +0 -10
- package/src/assets/README.md +0 -54
- package/src/components/App/App.tsx +0 -18
- package/src/components/App/index.ts +0 -1
- package/src/components/Footer/Footer.tsx +0 -16
- package/src/components/Footer/index.ts +0 -1
- package/src/components/Header/Header.tsx +0 -17
- package/src/components/Header/__tests__/__snapshots__/Header.test.tsx.snap +0 -37
- package/src/components/Header/index.ts +0 -1
- package/src/components/README.md +0 -33
- package/src/config/README.md +0 -63
- package/src/config/index.ts +0 -1
- package/src/forms/README.md +0 -142
- package/src/hooks/README.md +0 -83
- package/src/hooks/api/queryKey.ts +0 -1
- package/src/hooks/api/useDemoApi.ts +0 -16
- package/src/pages/Demo/Demo.tsx +0 -236
- package/src/pages/Demo/ReactHookFormDemo/index.ts +0 -1
- package/src/pages/Demo/ReactHookFormDemo/routes.tsx +0 -7
- package/src/pages/Demo/ReactQueryDemo/index.ts +0 -1
- package/src/pages/Demo/ReactQueryDemo/routes.tsx +0 -7
- package/src/pages/Demo/__tests__/Demo.test.tsx +0 -15
- package/src/pages/Demo/__tests__/__snapshots__/Demo.test.tsx.snap +0 -557
- package/src/pages/Demo/index.ts +0 -1
- package/src/pages/Demo/routes.tsx +0 -8
- package/src/pages/Layout/Layout.tsx +0 -12
- package/src/pages/Layout/index.ts +0 -1
- package/src/pages/README.md +0 -91
- package/src/pages/UnexpectedError/UnexpectedError.module.css +0 -8
- package/src/pages/UnexpectedError/UnexpectedError.tsx +0 -22
- package/src/pages/UnexpectedError/index.ts +0 -1
- package/src/providers/QueryClientProvider/index.ts +0 -1
- package/src/providers/README.md +0 -112
- package/src/providers/index.ts +0 -1
- package/src/providers/withProviders/index.ts +0 -1
- package/src/providers/withProviders/withProviders.tsx +0 -18
- package/src/routing/README.md +0 -76
- package/src/routing/appRoutes.ts +0 -5
- package/src/routing/routerFactory.ts +0 -12
- package/src/routing/routes.tsx +0 -13
- package/src/types/README.md +0 -62
- package/src/types/demoApi.ts +0 -25
- package/src/ui/README.md +0 -144
- package/src/ui/molecules/Navigation/index.ts +0 -1
- package/src/ui/organisms/Footer/Footer.module.css +0 -10
- package/src/ui/organisms/Footer/Footer.tsx +0 -10
- package/src/ui/organisms/Footer/index.ts +0 -1
- package/src/ui/organisms/Header/Header.module.css +0 -17
- package/src/ui/organisms/Header/Header.tsx +0 -16
- package/src/ui/organisms/Header/index.ts +0 -1
- package/src/ui/organisms/MainContent/MainContent.module.css +0 -3
- package/src/ui/organisms/MainContent/MainContent.tsx +0 -6
- package/src/ui/organisms/MainContent/index.ts +0 -1
- package/src/ui/templates/MainTemplate/MainTemplate.module.css +0 -5
- package/src/utils/README.md +0 -70
- /package/src/{assets → app/assets}/css/index.ts +0 -0
- /package/src/{assets → app/assets}/css/mainBody.css +0 -0
- /package/src/{assets → app/assets}/css/reset.css +0 -0
- /package/src/{config → app/config}/queryClientConfig.ts +0 -0
- /package/src/{assets → demo/ui/assets}/images/logo.png +0 -0
- /package/src/{assets → demo/ui/assets}/images/templatePlugins.png +0 -0
- /package/src/{components/Footer/Footer.module.css → demo/ui/components/DemoFooter/DemoFooter.module.css} +0 -0
- /package/src/{components/Header/Header.module.css → demo/ui/components/DemoHeader/DemoHeader.module.css} +0 -0
- /package/src/{ui/molecules/Navigation/Navigation.module.css → demo/ui/components/DemoNavigation/DemoNavigation.module.css} +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Input/Input.module.scss +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Input/Input.tsx +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Input/index.ts +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Logo/Logo.module.css +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Logo/Logo.tsx +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Logo/__tests__/Logo.test.tsx +0 -0
- /package/src/{ui/atoms → demo/ui/components}/Logo/index.ts +0 -0
- /package/src/{ui/atoms → demo/ui/components}/NavLink/NavLink.tsx +0 -0
- /package/src/{ui/atoms → demo/ui/components}/NavLink/index.ts +0 -0
- /package/src/{components → demo/ui/components}/TextInput/index.ts +0 -0
- /package/src/{forms → demo/ui/forms}/ProfileForm/ProfileForm.module.scss +0 -0
- /package/src/{forms → demo/ui/forms}/ProfileForm/index.ts +0 -0
- /package/src/{ui/templates → demo/ui/layouts}/MainTemplate/index.ts +0 -0
- /package/src/{pages/Demo/Demo.module.scss → demo/ui/pages/DemoHome/DemoHome.module.scss} +0 -0
- /package/src/{pages/Demo/ReactQueryDemo/ReactQueryDemo.module.scss → demo/ui/pages/DependencyInjectionDemo/DependencyInjectionDemo.module.scss} +0 -0
- /package/src/{pages/Demo/ReactHookFormDemo/ReactHookFormDemo.module.css → demo/ui/pages/ReactHookFormDemo/ReactHookFormDemo.module.scss} +0 -0
- /package/src/{api → shared/infrastructure/http}/constant.ts +0 -0
- /package/src/{types/api.ts → shared/infrastructure/http/fetchApi.type.d.ts} +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# `app`
|
|
2
|
+
|
|
3
|
+
The `app` folder is the **application bootstrap layer**. It is not a bounded context — it has no business
|
|
4
|
+
domain of its own. Its single responsibility is to assemble all the pieces (providers, router, DI container,
|
|
5
|
+
contexts) and expose the application to the browser.
|
|
6
|
+
|
|
7
|
+
Path alias: `@App/*`
|
|
8
|
+
|
|
9
|
+
## Contents
|
|
10
|
+
|
|
11
|
+
| Subfolder / file | Role |
|
|
12
|
+
| -------------------- | -------------------------------------------------------------------------------- |
|
|
13
|
+
| `App.tsx` | Root React component — composes providers and router |
|
|
14
|
+
| `AppHTMLElement.tsx` | Custom-element bootstrap (`<app-react>`). Mounts/unmounts the React root |
|
|
15
|
+
| `assets/` | Global CSS reset, base styles, and app-wide static assets |
|
|
16
|
+
| `config/` | App-wide configuration and the **composition root** (`serviceContainer.ts`) |
|
|
17
|
+
| `providers/` | React provider components: `ServiceProvider`, `QueryClientProvider`, `Providers` |
|
|
18
|
+
| `routing/` | Top-level router and route aggregation |
|
|
19
|
+
|
|
20
|
+
## Composition root
|
|
21
|
+
|
|
22
|
+
`config/serviceContainer.ts` is the **composition root** of the dependency injection system. It is the single
|
|
23
|
+
place in the application where the Inversify container is created and all bounded-context service collections
|
|
24
|
+
are loaded:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { serviceDemo } from '@Demo/serviceDemo';
|
|
28
|
+
import { Container } from 'inversify';
|
|
29
|
+
|
|
30
|
+
export const serviceContainer = new Container();
|
|
31
|
+
serviceContainer.load(serviceDemo);
|
|
32
|
+
// add more context modules here as the application grows
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The composition root is the **only** place that knows about concrete implementations. Every other layer receives
|
|
36
|
+
dependencies through interfaces and the DI container.
|
|
37
|
+
|
|
38
|
+
## Provider tree
|
|
39
|
+
|
|
40
|
+
The `Providers` component wraps the application in its provider tree. The order matters:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
ServiceProvider ← makes the DI container available via React context
|
|
44
|
+
└── QueryClientProvider ← makes React Query available
|
|
45
|
+
└── {children} ← the rest of the application
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`ServiceProvider` must be outermost so that any inner provider can use `useService` to resolve DI services.
|
|
49
|
+
|
|
50
|
+
## Custom element bootstrap
|
|
51
|
+
|
|
52
|
+
`AppHTMLElement.tsx` registers the application as a
|
|
53
|
+
[custom element](https://developer.mozilla.org/en-US/docs/Web/API/CustomElementRegistry/define) (`<app-react>`).
|
|
54
|
+
This allows the React app to be embedded in any HTML host without tight coupling to a specific framework or
|
|
55
|
+
page structure. The custom element mounts a React root in `connectedCallback` and unmounts it in
|
|
56
|
+
`disconnectedCallback`.
|
|
57
|
+
|
|
58
|
+
## Dependency rules for `app`
|
|
59
|
+
|
|
60
|
+
- ✅ Can import from `@Shared/*` and any bounded context (e.g., `@Demo/*`)
|
|
61
|
+
- ✅ Responsible for high-level orchestration only
|
|
62
|
+
- ❌ Must not contain business rules or feature-specific logic
|
|
63
|
+
- ❌ Generic helpers belong in `shared`, not here
|
|
64
|
+
|
|
65
|
+
## Avoid
|
|
66
|
+
|
|
67
|
+
- Business rules or domain objects
|
|
68
|
+
- Feature-specific orchestration that belongs to a bounded context
|
|
69
|
+
- Generic helpers that should live in `shared`
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { App } from '@
|
|
1
|
+
import { App } from '@App/App';
|
|
2
2
|
import { waitFor } from '@testing-library/react';
|
|
3
3
|
import { StrictMode } from 'react';
|
|
4
4
|
import { createRoot } from 'react-dom/client';
|
|
5
5
|
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest';
|
|
6
|
-
import
|
|
6
|
+
import AppHTMLElement from '../AppHTMLElement';
|
|
7
7
|
|
|
8
|
-
vi.mock('@
|
|
8
|
+
vi.mock('@App/App', () => ({
|
|
9
9
|
App: () => <div data-testid="app-mock">AppMock</div>,
|
|
10
10
|
}));
|
|
11
11
|
|
|
@@ -13,7 +13,7 @@ vi.mock('react-dom/client', () => ({
|
|
|
13
13
|
createRoot: vi.fn(),
|
|
14
14
|
}));
|
|
15
15
|
|
|
16
|
-
describe('
|
|
16
|
+
describe('AppHTMLElement', () => {
|
|
17
17
|
let container: HTMLElement;
|
|
18
18
|
const render = vi.fn();
|
|
19
19
|
const unmount = vi.fn();
|
|
@@ -22,7 +22,7 @@ describe('bootstrap', () => {
|
|
|
22
22
|
unmount,
|
|
23
23
|
}));
|
|
24
24
|
|
|
25
|
-
customElements.define('bootstrap-html-element',
|
|
25
|
+
customElements.define('bootstrap-html-element', AppHTMLElement);
|
|
26
26
|
|
|
27
27
|
beforeEach(() => {
|
|
28
28
|
container = document.createElement('bootstrap-html-element');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { QueryClientProvider } from '@App/providers/QueryClientProvider/QueryClientProvider';
|
|
2
|
+
import { ServiceProvider } from '@App/providers/ServiceProvider/ServiceProvider';
|
|
3
|
+
import type { QueryClient } from '@tanstack/react-query';
|
|
4
|
+
import type { Container } from 'inversify';
|
|
5
|
+
import type { PropsWithChildren } from 'react';
|
|
6
|
+
|
|
7
|
+
type ProvidersProps = PropsWithChildren<{
|
|
8
|
+
queryClient: QueryClient;
|
|
9
|
+
container: Container;
|
|
10
|
+
}>;
|
|
11
|
+
|
|
12
|
+
export const Providers = ({ queryClient, container, children }: ProvidersProps) => {
|
|
13
|
+
return (
|
|
14
|
+
<ServiceProvider container={container}>
|
|
15
|
+
<QueryClientProvider queryClient={queryClient}>{children}</QueryClientProvider>
|
|
16
|
+
</ServiceProvider>
|
|
17
|
+
);
|
|
18
|
+
};
|
|
@@ -3,12 +3,12 @@ import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
|
3
3
|
import type { PropsWithChildren } from 'react';
|
|
4
4
|
|
|
5
5
|
type ClientProviderProps = {
|
|
6
|
-
|
|
6
|
+
queryClient: QueryClient;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
export const QueryClientProvider = ({ children,
|
|
9
|
+
export const QueryClientProvider = ({ children, queryClient }: PropsWithChildren<ClientProviderProps>) => {
|
|
10
10
|
return (
|
|
11
|
-
<ReactQueryClientProvider client={
|
|
11
|
+
<ReactQueryClientProvider client={queryClient}>
|
|
12
12
|
{children}
|
|
13
13
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
14
14
|
</ReactQueryClientProvider>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ServiceContext } from '@App/providers/ServiceProvider/ServiceContext';
|
|
2
|
+
import type { Container } from 'inversify';
|
|
3
|
+
import type { PropsWithChildren } from 'react';
|
|
4
|
+
import { useRef } from 'react';
|
|
5
|
+
|
|
6
|
+
export type ServiceProviderProps = PropsWithChildren<{
|
|
7
|
+
container: Container;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export const ServiceProvider = ({ container, children }: ServiceProviderProps) => {
|
|
11
|
+
const containerRef = useRef({ container });
|
|
12
|
+
|
|
13
|
+
return <ServiceContext.Provider value={containerRef.current}>{children}</ServiceContext.Provider>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { RouterProvider, createBrowserRouter } from 'react-router';
|
|
2
|
+
import { routeObject } from './routes';
|
|
3
|
+
|
|
4
|
+
type RouterProps = {
|
|
5
|
+
basename?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const Router = ({ basename }: RouterProps = {}) => {
|
|
9
|
+
const router = createBrowserRouter(routeObject, {
|
|
10
|
+
basename,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return <RouterProvider router={router} />;
|
|
14
|
+
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# `demo`
|
|
2
|
+
|
|
3
|
+
The `demo` folder is the template's **example Bounded Context**.
|
|
4
|
+
|
|
5
|
+
Path alias: `@Demo/*`
|
|
6
|
+
|
|
7
|
+
## What is a Bounded Context?
|
|
8
|
+
|
|
9
|
+
In Domain-Driven Design (DDD), a **Bounded Context** is an isolated, autonomous slice of the application that
|
|
10
|
+
owns its own domain model, its own ubiquitous language, and its own internal implementation. It communicates
|
|
11
|
+
with the outside world (other contexts, the app layer) through well-defined entry points only.
|
|
12
|
+
|
|
13
|
+
Practical rules:
|
|
14
|
+
|
|
15
|
+
- A bounded context **never imports directly from another bounded context**. Cross-context communication goes
|
|
16
|
+
through clearly defined contracts (events, DTOs, or a shared kernel type).
|
|
17
|
+
- A bounded context **owns its full stack**: from the domain model all the way to the UI.
|
|
18
|
+
- Every concept inside a context is named consistently — this is the **Ubiquitous Language** principle. The
|
|
19
|
+
same word must mean the same thing everywhere inside the context.
|
|
20
|
+
|
|
21
|
+
## Layer structure
|
|
22
|
+
|
|
23
|
+
The `demo` context follows Clean Architecture's four-layer model:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
demo/
|
|
27
|
+
├── domain/ ← pure business contracts, entities, repository interfaces
|
|
28
|
+
├── application/ ← use cases, DTOs, orchestration
|
|
29
|
+
├── infrastructure/ ← concrete implementations, adapters
|
|
30
|
+
├── ui/ ← React pages, components, hooks, routing
|
|
31
|
+
├── serviceDemo.ts ← Inversify ContainerModule that wires this context's DI
|
|
32
|
+
└── serviceIdentifiers.ts ← unique Symbols for each service in this context
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### `domain/`
|
|
36
|
+
|
|
37
|
+
The heart of the context. Contains:
|
|
38
|
+
|
|
39
|
+
- **Repository interfaces** — describe the data access operations the domain needs (e.g.,
|
|
40
|
+
`DemoUserRepositoryInterface`, `ArchitecturePrincipleRepositoryInterface`). These are TypeScript interfaces;
|
|
41
|
+
the implementation lives in `infrastructure/`.
|
|
42
|
+
- **Entities** — objects with identity and lifecycle (e.g., `DemoUser`).
|
|
43
|
+
- **Value Objects** — immutable objects defined by their values (no identity).
|
|
44
|
+
|
|
45
|
+
Zero external dependencies. No React, no Inversify, no HTTP.
|
|
46
|
+
|
|
47
|
+
### `application/`
|
|
48
|
+
|
|
49
|
+
The orchestration layer. Contains:
|
|
50
|
+
|
|
51
|
+
- **Use cases** — named, testable business operations (e.g., `GetDemoUsersUseCase`). Each use case implements
|
|
52
|
+
an interface (`GetDemoUsersUseCaseInterface`) so it can be replaced by a test double without modifying the UI.
|
|
53
|
+
- **DTOs** — plain, typed structures that carry data across layer boundaries (e.g., `ArchitecturePrinciple`,
|
|
54
|
+
`ProfileFormValues`).
|
|
55
|
+
|
|
56
|
+
Can import from `domain`. Must not import from `infrastructure` or `ui`.
|
|
57
|
+
|
|
58
|
+
### `infrastructure/`
|
|
59
|
+
|
|
60
|
+
The adapter layer. Contains:
|
|
61
|
+
|
|
62
|
+
- **Concrete repository implementations** (e.g., `HttpDemoUserRepository`, `StaticArchitecturePrincipleRepository`).
|
|
63
|
+
Each class implements an interface defined in `domain/`, satisfying the **Dependency Inversion Principle**.
|
|
64
|
+
|
|
65
|
+
Can import from `domain` (the interfaces it implements) and from `@Shared/infrastructure` (shared adapters).
|
|
66
|
+
Must not import from `application` or `ui`.
|
|
67
|
+
|
|
68
|
+
### `ui/`
|
|
69
|
+
|
|
70
|
+
The presentation layer. Contains:
|
|
71
|
+
|
|
72
|
+
- **Pages** — top-level route components
|
|
73
|
+
- **Components** — context-specific visual components
|
|
74
|
+
- **Layouts** — page-level layout wrappers
|
|
75
|
+
- **Forms** — form composition (React Hook Form)
|
|
76
|
+
- **Hooks** — React Query hooks that bridge use cases to the React component tree
|
|
77
|
+
- **Routing** — route declarations exported to `@App/routing`
|
|
78
|
+
|
|
79
|
+
The UI layer uses `useService` from `@Shared/ui/hooks/useService` to resolve use cases from the DI container.
|
|
80
|
+
It calls use cases; it does not call repositories or infrastructure directly.
|
|
81
|
+
|
|
82
|
+
## Dependency direction within the context
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
ui → application → domain
|
|
86
|
+
infrastructure -.→ domain (implements its interfaces)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The arrows show allowed import direction. Reversing them is a Clean Architecture violation.
|
|
90
|
+
|
|
91
|
+
## DI wiring
|
|
92
|
+
|
|
93
|
+
### `serviceIdentifiers.ts`
|
|
94
|
+
|
|
95
|
+
Declares unique `Symbol` identifiers for every service in the context:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
export const DEMO_SERVICES = Object.freeze({
|
|
99
|
+
DemoUserRepository: Symbol.for('Demo.DemoUserRepository'),
|
|
100
|
+
GetDemoUsersUseCase: Symbol.for('Demo.GetDemoUsersUseCase'),
|
|
101
|
+
// ...
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Using `Symbol.for` with a namespaced string (`Demo.*`) avoids collisions with other contexts.
|
|
106
|
+
|
|
107
|
+
### `serviceDemo.ts`
|
|
108
|
+
|
|
109
|
+
A `ContainerModule` that binds each service identifier to its concrete implementation using Inversify's
|
|
110
|
+
`toDynamicValue`. This pattern avoids TypeScript decorators entirely, keeping the domain and application layers
|
|
111
|
+
free of framework metadata:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
export const serviceDemo: ContainerModule = new ContainerModule(options => {
|
|
115
|
+
options
|
|
116
|
+
.bind<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)
|
|
117
|
+
.toDynamicValue(() => new HttpDemoUserRepository())
|
|
118
|
+
.inSingletonScope();
|
|
119
|
+
|
|
120
|
+
options
|
|
121
|
+
.bind<GetDemoUsersUseCaseInterface>(DEMO_SERVICES.GetDemoUsersUseCase)
|
|
122
|
+
.toDynamicValue(
|
|
123
|
+
services => new GetDemoUsersUseCase(services.get<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)),
|
|
124
|
+
)
|
|
125
|
+
.inSingletonScope();
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`serviceDemo` is loaded into the global container in `@App/config/serviceContainer.ts`.
|
|
130
|
+
|
|
131
|
+
## UI organization principle
|
|
132
|
+
|
|
133
|
+
The UI layer uses practical, readable sub-folders (`components`, `layouts`, `pages`, `forms`, `hooks`,
|
|
134
|
+
`routing`). Atomic Design can still guide how you think about component responsibility — keeping components
|
|
135
|
+
small and composable — but strict `atoms/molecules/organisms/templates` directories add indirection without
|
|
136
|
+
real value in most projects.
|
|
137
|
+
|
|
138
|
+
## Important: demo as a model, not a constraint
|
|
139
|
+
|
|
140
|
+
The `demo` context exists to show the path. When you start your real project:
|
|
141
|
+
|
|
142
|
+
1. Rename or replace `demo` with your first real bounded context.
|
|
143
|
+
2. Add its alias to `tsconfig.json` (e.g., `"@Orders/*": ["src/orders/*"]`).
|
|
144
|
+
3. Create its `serviceIdentifiers.ts` and `service[ContextName].ts` (e.g., `serviceHome.ts`).
|
|
145
|
+
4. Register its container module in `app/config/serviceContainer.ts`.
|
|
146
|
+
|
|
147
|
+
Keep visual components in their context's `ui/` folder until there is a proven, concrete reason to move them
|
|
148
|
+
to `shared`.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ArchitecturePrinciple } from '@Demo/application/dtos/ArchitecturePrinciple';
|
|
2
|
+
import type { GetArchitecturePrinciplesUseCaseInterface } from '@Demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface';
|
|
3
|
+
import type { ArchitecturePrincipleRepositoryInterface } from '@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface';
|
|
4
|
+
|
|
5
|
+
export class GetArchitecturePrinciplesUseCase implements GetArchitecturePrinciplesUseCaseInterface {
|
|
6
|
+
public constructor(private readonly architecturePrincipleRepository: ArchitecturePrincipleRepositoryInterface) {}
|
|
7
|
+
|
|
8
|
+
public readonly execute = (): ArchitecturePrinciple[] => {
|
|
9
|
+
return this.architecturePrincipleRepository.getAll();
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { GetDemoUsersUseCaseInterface } from '@Demo/application/use-cases/GetDemoUsersUseCaseInterface';
|
|
2
|
+
import type { DemoUser } from '@Demo/domain/entities/DemoUser';
|
|
3
|
+
import type { DemoUserRepositoryInterface } from '@Demo/domain/repositories/DemoUserRepositoryInterface';
|
|
4
|
+
|
|
5
|
+
export class GetDemoUsersUseCase implements GetDemoUsersUseCaseInterface {
|
|
6
|
+
public constructor(private readonly demoUserRepository: DemoUserRepositoryInterface) {}
|
|
7
|
+
|
|
8
|
+
public readonly execute = async (): Promise<DemoUser[]> => {
|
|
9
|
+
return this.demoUserRepository.getAll();
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DemoUser } from '@Demo/domain/entities/DemoUser';
|
|
2
|
+
import type { DemoUserRepositoryInterface } from '@Demo/domain/repositories/DemoUserRepositoryInterface';
|
|
3
|
+
import { fetchApi } from '@Shared/infrastructure/http/fetchApi';
|
|
4
|
+
|
|
5
|
+
type DemoUserApiResponse = {
|
|
6
|
+
id: number;
|
|
7
|
+
name: string;
|
|
8
|
+
username: string;
|
|
9
|
+
email: string;
|
|
10
|
+
website: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export class HttpDemoUserRepository implements DemoUserRepositoryInterface {
|
|
14
|
+
private readonly apiPath = 'https://jsonplaceholder.typicode.com/users';
|
|
15
|
+
|
|
16
|
+
public readonly getAll = async (): Promise<DemoUser[]> => {
|
|
17
|
+
const users = await fetchApi<DemoUserApiResponse[]>({
|
|
18
|
+
path: this.apiPath,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
return users.map(user => ({
|
|
22
|
+
id: user.id,
|
|
23
|
+
name: user.name,
|
|
24
|
+
username: user.username.toUpperCase(),
|
|
25
|
+
email: user.email.toLowerCase(),
|
|
26
|
+
website: user.website,
|
|
27
|
+
}));
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ArchitecturePrinciple } from '@Demo/application/dtos/ArchitecturePrinciple';
|
|
2
|
+
import type { ArchitecturePrincipleRepositoryInterface } from '@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface';
|
|
3
|
+
|
|
4
|
+
export class StaticArchitecturePrincipleRepository implements ArchitecturePrincipleRepositoryInterface {
|
|
5
|
+
private readonly architecturePrinciples: ArchitecturePrinciple[] = [
|
|
6
|
+
{
|
|
7
|
+
title: 'Domain stays framework-free',
|
|
8
|
+
description: 'The demo domain only defines contracts and pure data structures. React and Inversify stay outside.',
|
|
9
|
+
layer: 'domain',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
title: 'Application orchestrates use cases',
|
|
13
|
+
description: 'Use cases expose intent-driven APIs such as GetDemoUsersUseCase instead of leaking infrastructure.',
|
|
14
|
+
layer: 'application',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
title: 'Infrastructure implements contracts',
|
|
18
|
+
description:
|
|
19
|
+
'Concrete adapters, such as the HTTP repository, live in infrastructure and are selected by the container.',
|
|
20
|
+
layer: 'infrastructure',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
title: 'UI depends on abstractions',
|
|
24
|
+
description:
|
|
25
|
+
'React pages use hooks and useService to resolve use cases without importing infrastructure directly.',
|
|
26
|
+
layer: 'ui',
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
public readonly getAll = (): ArchitecturePrinciple[] => {
|
|
31
|
+
return this.architecturePrinciples;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ArchitecturePrincipleRepositoryInterface } from '@Demo/domain/repositories/ArchitecturePrincipleRepositoryInterface';
|
|
2
|
+
import type { DemoUserRepositoryInterface } from '@Demo/domain/repositories/DemoUserRepositoryInterface';
|
|
3
|
+
import { StaticArchitecturePrincipleRepository } from '@Demo/infrastructure/repositories/StaticArchitecturePrincipleRepository';
|
|
4
|
+
import { HttpDemoUserRepository } from '@Demo/infrastructure/repositories/HttpDemoUserRepository';
|
|
5
|
+
import { GetArchitecturePrinciplesUseCase } from '@Demo/application/use-cases/GetArchitecturePrinciplesUseCase';
|
|
6
|
+
import type { GetArchitecturePrinciplesUseCaseInterface } from '@Demo/application/use-cases/GetArchitecturePrinciplesUseCaseInterface';
|
|
7
|
+
import { GetDemoUsersUseCase } from '@Demo/application/use-cases/GetDemoUsersUseCase';
|
|
8
|
+
import type { GetDemoUsersUseCaseInterface } from '@Demo/application/use-cases/GetDemoUsersUseCaseInterface';
|
|
9
|
+
import { ContainerModule } from 'inversify';
|
|
10
|
+
import { DEMO_SERVICES } from './serviceIdentifiers';
|
|
11
|
+
|
|
12
|
+
export const serviceDemo: ContainerModule = new ContainerModule(options => {
|
|
13
|
+
options
|
|
14
|
+
.bind<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)
|
|
15
|
+
.toDynamicValue(() => new HttpDemoUserRepository())
|
|
16
|
+
.inSingletonScope();
|
|
17
|
+
|
|
18
|
+
options
|
|
19
|
+
.bind<GetDemoUsersUseCaseInterface>(DEMO_SERVICES.GetDemoUsersUseCase)
|
|
20
|
+
.toDynamicValue(
|
|
21
|
+
services => new GetDemoUsersUseCase(services.get<DemoUserRepositoryInterface>(DEMO_SERVICES.DemoUserRepository)),
|
|
22
|
+
)
|
|
23
|
+
.inSingletonScope();
|
|
24
|
+
|
|
25
|
+
options
|
|
26
|
+
.bind<ArchitecturePrincipleRepositoryInterface>(DEMO_SERVICES.ArchitecturePrincipleRepository)
|
|
27
|
+
.toDynamicValue(() => new StaticArchitecturePrincipleRepository())
|
|
28
|
+
.inSingletonScope();
|
|
29
|
+
|
|
30
|
+
options
|
|
31
|
+
.bind<GetArchitecturePrinciplesUseCaseInterface>(DEMO_SERVICES.GetArchitecturePrinciplesUseCase)
|
|
32
|
+
.toDynamicValue(
|
|
33
|
+
services =>
|
|
34
|
+
new GetArchitecturePrinciplesUseCase(
|
|
35
|
+
services.get<ArchitecturePrincipleRepositoryInterface>(DEMO_SERVICES.ArchitecturePrincipleRepository),
|
|
36
|
+
),
|
|
37
|
+
)
|
|
38
|
+
.inSingletonScope();
|
|
39
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export const DEMO_SERVICES = Object.freeze({
|
|
2
|
+
DemoUserRepository: Symbol.for('Demo.DemoUserRepository'),
|
|
3
|
+
GetDemoUsersUseCase: Symbol.for('Demo.GetDemoUsersUseCase'),
|
|
4
|
+
ArchitecturePrincipleRepository: Symbol.for('Demo.ArchitecturePrincipleRepository'),
|
|
5
|
+
GetArchitecturePrinciplesUseCase: Symbol.for('Demo.GetArchitecturePrinciplesUseCase'),
|
|
6
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import logo from '@Demo/ui/assets/images/logo.png';
|
|
2
|
+
import { demoAppRoutes } from '@Demo/ui/routing/demoAppRoutes';
|
|
3
|
+
import { Logo } from '@Demo/ui/components/Logo/Logo';
|
|
4
|
+
import { Link } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import classes from './DemoFooter.module.css';
|
|
7
|
+
|
|
8
|
+
export const DemoFooter = () => {
|
|
9
|
+
return (
|
|
10
|
+
<div className={classes.container}>
|
|
11
|
+
<hr />
|
|
12
|
+
<Link to={demoAppRoutes.home}>
|
|
13
|
+
<Logo size="50px" src={logo} alt="logo" />
|
|
14
|
+
</Link>
|
|
15
|
+
</div>
|
|
16
|
+
);
|
|
17
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
import { BrowserRouter as Router } from 'react-router';
|
|
3
|
-
import {
|
|
3
|
+
import { DemoFooter } from '../DemoFooter';
|
|
4
4
|
|
|
5
5
|
describe('Footer Component', () => {
|
|
6
6
|
it('should render the Footer component correctly', () => {
|
|
7
7
|
const { container } = render(
|
|
8
8
|
<Router>
|
|
9
|
-
<
|
|
9
|
+
<DemoFooter />
|
|
10
10
|
</Router>,
|
|
11
11
|
);
|
|
12
12
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
exports[`Footer Component > should render the Footer component correctly 1`] = `
|
|
4
4
|
<div>
|
|
5
5
|
<div
|
|
6
|
-
class="
|
|
6
|
+
class="container"
|
|
7
7
|
>
|
|
8
8
|
<hr />
|
|
9
9
|
<a
|
|
@@ -12,8 +12,8 @@ exports[`Footer Component > should render the Footer component correctly 1`] = `
|
|
|
12
12
|
>
|
|
13
13
|
<img
|
|
14
14
|
alt="logo"
|
|
15
|
-
class="
|
|
16
|
-
src="/src/assets/images/logo.png"
|
|
15
|
+
class="logoStyle"
|
|
16
|
+
src="/src/demo/ui/assets/images/logo.png"
|
|
17
17
|
style="width: 50px;"
|
|
18
18
|
/>
|
|
19
19
|
</a>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { DemoFooter } from './DemoFooter';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logo from '@Demo/ui/assets/images/logo.png';
|
|
2
|
+
import { DemoNavigation } from '@Demo/ui/components/DemoNavigation/DemoNavigation';
|
|
3
|
+
import { demoAppRoutes } from '@Demo/ui/routing/demoAppRoutes';
|
|
4
|
+
import { Logo } from '@Demo/ui/components/Logo/Logo';
|
|
5
|
+
import { Link } from 'react-router';
|
|
6
|
+
|
|
7
|
+
import classes from './DemoHeader.module.css';
|
|
8
|
+
|
|
9
|
+
export const DemoHeader = () => {
|
|
10
|
+
return (
|
|
11
|
+
<div className={classes.container}>
|
|
12
|
+
<Link to={demoAppRoutes.home}>
|
|
13
|
+
<Logo size="50px" src={logo} alt="logo" />
|
|
14
|
+
</Link>
|
|
15
|
+
<DemoNavigation
|
|
16
|
+
links={[
|
|
17
|
+
{ label: 'Overview', url: demoAppRoutes.home },
|
|
18
|
+
{ label: 'React Query', url: demoAppRoutes.reactQuery },
|
|
19
|
+
{ label: 'React Hook Form', url: demoAppRoutes.reactHookForm },
|
|
20
|
+
{ label: 'Dependency Injection', url: demoAppRoutes.dependencyInjection },
|
|
21
|
+
]}
|
|
22
|
+
/>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { render } from '@testing-library/react';
|
|
2
2
|
import { BrowserRouter as Router } from 'react-router';
|
|
3
|
-
import {
|
|
3
|
+
import { DemoHeader } from '../DemoHeader';
|
|
4
4
|
|
|
5
5
|
describe('Header Component', () => {
|
|
6
6
|
it('should render the Header component correctly', () => {
|
|
7
7
|
const { container } = render(
|
|
8
8
|
<Router>
|
|
9
|
-
<
|
|
9
|
+
<DemoHeader />
|
|
10
10
|
</Router>,
|
|
11
11
|
);
|
|
12
12
|
|