@jeffrey2423/coding-standards 1.0.0 → 2.0.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/README.md +95 -174
- package/bin/cli.js +373 -20
- package/package.json +13 -3
- package/standards/backend/architecture/event-driven.md +112 -0
- package/standards/backend/architecture/microservice-anatomy.md +106 -0
- package/standards/backend/architecture/multitenancy.md +112 -0
- package/standards/backend/architecture/public-api-facade.md +112 -0
- package/standards/backend/architecture/shared-vs-owned.md +62 -0
- package/standards/{backend-standards.md → backend/backend-standards.md} +8 -1
- package/standards/{database-conventions.md → backend/database-conventions.md} +7 -0
- package/standards/backend/technology-stack.md +73 -0
- package/standards/core/ai-collaboration.md +64 -0
- package/standards/core/clean-architecture-ddd.md +69 -0
- package/standards/core/coding-conventions.md +66 -0
- package/standards/core/testing-strategy.md +46 -0
- package/standards/{mobile-flutter-standards.md → mobile/flutter/flutter-standards.md} +9 -1
- package/standards/{mobile-react-native-standards.md → mobile/react-native/react-native-standards.md} +9 -1
- package/standards/{technical-preferences-ux.md → web/_base/design-system-ux.md} +8 -1
- package/standards/web/_base/frontend-architecture.md +75 -0
- package/standards/{frontend-standards.md → web/_base/frontend-standards.md} +7 -0
- package/standards/web/_base/technology-stack.md +40 -0
- package/standards/web/microfrontends/module-federation-standard.md +216 -0
- package/standards/web/single-spa/single-spa-standard.md +196 -0
- package/standards/web/spa/spa-standard.md +53 -0
- package/standards/architecture-patterns.md +0 -444
- package/standards/technology-stack.md +0 -294
- package/standards/vite-config-standard.md +0 -531
package/standards/{mobile-react-native-standards.md → mobile/react-native/react-native-standards.md}
RENAMED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: React Native Development Standards
|
|
3
|
+
platform: mobile
|
|
4
|
+
track: react-native
|
|
5
|
+
load_when: "Building a React Native app — Expo, Zustand, Expo Router, NativeWind."
|
|
6
|
+
updated: 2026-06
|
|
7
|
+
---
|
|
8
|
+
|
|
1
9
|
# Mobile React Native Development Standards
|
|
2
10
|
|
|
3
|
-
> **Note**: For shared architectural principles (Clean Architecture, DDD
|
|
11
|
+
> **Note**: For shared architectural principles (Clean Architecture, DDD), refer to [`../../core/clean-architecture-ddd.md`](../../core/clean-architecture-ddd.md). For design system (colors, typography, UX), refer to [`../../web/_base/design-system-ux.md`](../../web/_base/design-system-ux.md). React Native shares the same core architecture philosophy as the web frontend standards in [`../../web/_base/frontend-standards.md`](../../web/_base/frontend-standards.md) — adapted for native mobile.
|
|
4
12
|
|
|
5
13
|
---
|
|
6
14
|
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
---
|
|
2
|
+
title: Design System & UX
|
|
3
|
+
platform: web
|
|
4
|
+
load_when: "UI/visual work — colors, typography, spacing, accessibility, performance targets."
|
|
5
|
+
updated: 2026-06
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Design System & UX
|
|
2
9
|
|
|
3
10
|
## Project Setup Configuration
|
|
4
11
|
Based on Frontend Development Standards and Clean Architecture Implementation
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Frontend Architecture
|
|
3
|
+
platform: web
|
|
4
|
+
load_when: "Any web work — defines folder structure, routing conventions, and how to pick a web track."
|
|
5
|
+
updated: 2026-06
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Frontend Architecture
|
|
9
|
+
|
|
10
|
+
Applies to every web track (SPA, Single-SPA, Module Federation). Implements [`core/clean-architecture-ddd.md`](../../core/clean-architecture-ddd.md) on the frontend.
|
|
11
|
+
|
|
12
|
+
## Choosing a web track
|
|
13
|
+
|
|
14
|
+
These are **independent options**, not a default-plus-exceptions. Pick by need:
|
|
15
|
+
|
|
16
|
+
| Track | Choose when | Doc |
|
|
17
|
+
|---|---|---|
|
|
18
|
+
| **SPA** | One cohesive app, single deployable | [`web/spa/spa-standard.md`](../spa/spa-standard.md) |
|
|
19
|
+
| **Single-SPA** | Independently-deployed modules, possibly **mixed frameworks**, hard lifecycle isolation | [`web/single-spa/single-spa-standard.md`](../single-spa/single-spa-standard.md) |
|
|
20
|
+
| **Module Federation** | Homogeneous React, capabilities reused across products, **license-gated runtime composition** | [`web/microfrontends/module-federation-standard.md`](../microfrontends/module-federation-standard.md) |
|
|
21
|
+
|
|
22
|
+
Default to **SPA** until a concrete need justifies microfrontend complexity. Don't adopt it speculatively.
|
|
23
|
+
|
|
24
|
+
## Folder structure
|
|
25
|
+
|
|
26
|
+
Organize by **business module → domain → feature**, with Clean Architecture layers inside each feature:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
src/
|
|
30
|
+
├── main.tsx # entry point
|
|
31
|
+
├── routes/ # TanStack Router (route definitions only)
|
|
32
|
+
│ ├── __root.tsx # root layout (providers)
|
|
33
|
+
│ ├── _auth.tsx # pathless public layout
|
|
34
|
+
│ └── _app.tsx # pathless protected layout
|
|
35
|
+
├── modules/ # business logic
|
|
36
|
+
│ └── sales/ # MODULE
|
|
37
|
+
│ └── quotes/ # DOMAIN
|
|
38
|
+
│ └── cart/ # FEATURE
|
|
39
|
+
│ ├── domain/ # entities, repo interfaces, types
|
|
40
|
+
│ ├── application/ # use-cases, hooks, store
|
|
41
|
+
│ ├── infrastructure/ # repo impls, api, adapters
|
|
42
|
+
│ └── presentation/ # components
|
|
43
|
+
├── shared/ # reusable components, hooks, lib, types
|
|
44
|
+
├── app/ # global providers + stores
|
|
45
|
+
├── infrastructure/ # global services (api, storage, pwa)
|
|
46
|
+
└── styles/ # global styles
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## TanStack Router conventions
|
|
50
|
+
|
|
51
|
+
| Prefix | Effect | Example |
|
|
52
|
+
|---|---|---|
|
|
53
|
+
| `_` | Pathless layout (no URL segment) | `_app.tsx` |
|
|
54
|
+
| `.` | Flat routing | `orders.$id.tsx` → `/orders/:id` |
|
|
55
|
+
| `-` | Ignored by router (colocated files) | `-components/` |
|
|
56
|
+
| `$` | Dynamic parameter | `$orderId.tsx` → `:orderId` |
|
|
57
|
+
|
|
58
|
+
## Core rules
|
|
59
|
+
|
|
60
|
+
- **MUST** keep `routes/` for route definitions only; business logic lives in `modules/`.
|
|
61
|
+
- **MUST** split state by ownership: **Zustand** (global client) / **TanStack Query** (server) / `useState` (local).
|
|
62
|
+
- **MUST** keep `routeTree.gen.ts` untouched (auto-generated).
|
|
63
|
+
- **MUST** meet WCAG 2.1 AA in every component.
|
|
64
|
+
- **SHOULD** lazy-load routes (automatic with TanStack Router `autoCodeSplitting`).
|
|
65
|
+
- **SHOULD** build complex UI by composing small reusable components.
|
|
66
|
+
|
|
67
|
+
## State management split
|
|
68
|
+
|
|
69
|
+
| State | Tool | Example |
|
|
70
|
+
|---|---|---|
|
|
71
|
+
| Server data (fetch/cache/sync) | TanStack Query | product list, order detail |
|
|
72
|
+
| Global client state | Zustand | auth session, theme, cart |
|
|
73
|
+
| Local UI state | `useState`/`useReducer` | modal open, form field focus |
|
|
74
|
+
|
|
75
|
+
See [`frontend-standards.md`](frontend-standards.md) for detailed component, form, and testing rules.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Web Technology Stack
|
|
3
|
+
platform: web
|
|
4
|
+
load_when: "Any web work — the approved frontend toolchain and versions."
|
|
5
|
+
updated: 2026-06
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Web Technology Stack
|
|
9
|
+
|
|
10
|
+
The approved frontend toolchain. Use these by default; deviations need a stated reason.
|
|
11
|
+
|
|
12
|
+
| Category | Technology | Version | Notes |
|
|
13
|
+
|---|---|---|---|
|
|
14
|
+
| Bundler | Vite | 7+ | native ESM, fast HMR |
|
|
15
|
+
| Framework | React | 19 (18+ supported) | functional components + hooks |
|
|
16
|
+
| Language | TypeScript | 5+ | `strict` mode mandatory |
|
|
17
|
+
| Routing | TanStack Router | 1+ | file-based, type-safe, auto code-splitting |
|
|
18
|
+
| Client state | Zustand | 5+ | feature-based stores |
|
|
19
|
+
| Server state | TanStack Query | 5+ | cache, sync, optimistic updates |
|
|
20
|
+
| UI components | shadcn/ui + Radix UI | latest | install shadcn via MCP, not by hand |
|
|
21
|
+
| Styling | TailwindCSS | v4 | utility-first; design tokens from the UI kit |
|
|
22
|
+
| Forms | React Hook Form + Zod | latest | schema-driven validation |
|
|
23
|
+
| HTTP | Axios | latest | with interceptors |
|
|
24
|
+
| Testing | Vitest + React Testing Library + MSW | latest | MSW for API mocking |
|
|
25
|
+
| PWA | `vite-plugin-pwa` + Workbox | latest | offline-first where required |
|
|
26
|
+
|
|
27
|
+
## Rules
|
|
28
|
+
|
|
29
|
+
- **MUST** use TypeScript `strict`; no implicit `any`.
|
|
30
|
+
- **MUST** validate forms with Zod schemas; never trust client input.
|
|
31
|
+
- **MUST** keep the initial bundle < 500KB gzipped (see performance targets in [`design-system-ux.md`](design-system-ux.md)).
|
|
32
|
+
- **SHOULD** install shadcn/ui components via the MCP integration rather than hand-copying.
|
|
33
|
+
- **SHOULD** prefer TanStack Query for all server state instead of bespoke fetch-in-effect logic.
|
|
34
|
+
|
|
35
|
+
## Microfrontend dependency sharing
|
|
36
|
+
|
|
37
|
+
When using a microfrontend track, shared dependencies (React, router, query client, Zustand) **MUST** be declared `singleton: true` with a `requiredVersion` to prevent duplicate runtime instances. Track-specific configuration:
|
|
38
|
+
|
|
39
|
+
- Module Federation → [`web/microfrontends/module-federation-standard.md`](../microfrontends/module-federation-standard.md)
|
|
40
|
+
- Single-SPA → [`web/single-spa/single-spa-standard.md`](../single-spa/single-spa-standard.md)
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Module Federation Microfrontends Standard
|
|
3
|
+
platform: web
|
|
4
|
+
track: microfrontends
|
|
5
|
+
load_when: "Building a homogeneous (all-React) multi-product web platform where capabilities are reused across products and modules are enabled/disabled per license without redeploy."
|
|
6
|
+
updated: 2026-06
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Module Federation Microfrontends Standard
|
|
10
|
+
|
|
11
|
+
> **Choose this track when** the whole frontend is a **homogeneous React stack** and you want: efficient sharing of singleton dependencies, end-to-end TypeScript typing across module boundaries, native code-splitting, and **runtime composition driven by the backend** (per-tenant licensing). If you must mix frameworks or need hard per-module lifecycle isolation, use the [Single-SPA track](../single-spa/single-spa-standard.md) instead.
|
|
12
|
+
|
|
13
|
+
This standard uses **Module Federation 2.0** (`@module-federation/enhanced`), whose runtime is decoupled from the bundler into a standalone SDK. That decoupling is what makes **dynamic, license-gated remote loading** possible: the host asks the backend which remotes to load and registers them at runtime.
|
|
14
|
+
|
|
15
|
+
## The two axes
|
|
16
|
+
|
|
17
|
+
A multi-product platform has two **independent** dimensions. Conflating them is the classic mistake.
|
|
18
|
+
|
|
19
|
+
| | Capability 1 | Capability 2 | Capability 3 | Capability 4 |
|
|
20
|
+
|--------------|:---:|:---:|:---:|:---:|
|
|
21
|
+
| **Product A** | ✅ | ✅ | | |
|
|
22
|
+
| **Product B** | ✅ | | ✅ | |
|
|
23
|
+
| **Product C** | | ✅ | | |
|
|
24
|
+
| **Product D** | | | | ✅ |
|
|
25
|
+
|
|
26
|
+
- **Capabilities** (horizontal) = **remote MFEs**. Built, deployed and versioned independently. Written **once**, reused across products.
|
|
27
|
+
- **Products** (vertical) = **router layouts inside the single shell**. They compose capabilities and pass them props. Creating a new product = adding a folder with a `route.tsx`, not a new app.
|
|
28
|
+
|
|
29
|
+
## Core rules
|
|
30
|
+
|
|
31
|
+
- **MUST** use a **single shell, single router, single session**. Never nest a shell inside a shell.
|
|
32
|
+
- **MUST** model products as **route layouts** in the shell; model capabilities as **remote MFEs**.
|
|
33
|
+
- **MUST** keep capabilities **product-agnostic**: a remote receives typed props and adapts; it never knows which product hosts it.
|
|
34
|
+
- **MUST** declare shared singletons (`react`, `react-dom`, router, query client, UI kit, contracts) with `singleton: true` and a `requiredVersion`. Symmetric config between shell and every remote.
|
|
35
|
+
- **MUST** decide which remotes load from a **backend manifest endpoint** keyed by tenant/license — the frontend never hardcodes that decision.
|
|
36
|
+
- **MUST** guard products and capabilities with declarative two-level license guards (`beforeLoad`).
|
|
37
|
+
- **MUST** version the shared `@org/contracts` package semantically; a breaking prop change is a new major.
|
|
38
|
+
- **SHOULD** wrap remote loading in `Suspense` + an `ErrorBoundary` with an offline fallback (CDN failure is a real risk).
|
|
39
|
+
- **SHOULD NOT** create "private" props or events the contracts package doesn't describe — that breaks parity and typing.
|
|
40
|
+
|
|
41
|
+
## Stack (2026)
|
|
42
|
+
|
|
43
|
+
| Concern | Choice |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Federation | **Module Federation 2.0** — `@module-federation/enhanced` (runtime SDK + `mf-manifest.json`) |
|
|
46
|
+
| Bundler | Vite 7+ (`@module-federation/vite`) or Rspack (`@module-federation/rsbuild-plugin`) |
|
|
47
|
+
| Framework | React 19 + TypeScript strict |
|
|
48
|
+
| Router | TanStack Router 1+ (typed nested layouts) |
|
|
49
|
+
| Server state | TanStack Query 5+ (singleton) |
|
|
50
|
+
| Shared packages | `@org/contracts` (types/props), `@org/ui-kit` (design system), `@org/license` (entitlements) |
|
|
51
|
+
|
|
52
|
+
## Architecture in three layers
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
SHELL (single host)
|
|
56
|
+
• boots, fetches license + dynamic manifest
|
|
57
|
+
• defines product layouts, owns global routing + guards
|
|
58
|
+
│ Module Federation 2.0 runtime (registerRemotes / loadRemote)
|
|
59
|
+
▼
|
|
60
|
+
CAPABILITY MFEs (remotes, deployed independently)
|
|
61
|
+
mfe-capacidad-1 mfe-capacidad-2 mfe-capacidad-3 ...
|
|
62
|
+
│ build-time imports
|
|
63
|
+
▼
|
|
64
|
+
SHARED PACKAGES (npm, singleton at runtime)
|
|
65
|
+
@org/contracts @org/ui-kit @org/license
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Products as router layouts
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
// shell/src/routes/producto-a/route.tsx
|
|
72
|
+
export const Route = createFileRoute('/producto-a')({
|
|
73
|
+
beforeLoad: () => requireProduct('producto-a'), // product-level license guard
|
|
74
|
+
component: () => (
|
|
75
|
+
<div className="producto-a-layout">
|
|
76
|
+
<ProductoAHeader />
|
|
77
|
+
<ProductoASidebar />
|
|
78
|
+
<main><Outlet /></main> {/* a capability renders here */}
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// shell/src/routes/producto-a/capacidad-1.tsx
|
|
86
|
+
import { loadRemote } from '@module-federation/enhanced/runtime';
|
|
87
|
+
const Capacidad1 = lazy(() => loadRemote('mfe_capacidad_1/View'));
|
|
88
|
+
|
|
89
|
+
export const Route = createFileRoute('/producto-a/capacidad-1')({
|
|
90
|
+
beforeLoad: () => requireFeature('capacidad-1'), // capability-level guard
|
|
91
|
+
component: () => (
|
|
92
|
+
<Suspense fallback={<CapabilitySkeleton />}>
|
|
93
|
+
<Capacidad1 mode="producto-a" showExtraContext />
|
|
94
|
+
</Suspense>
|
|
95
|
+
),
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The remote consumes typed props from `@org/contracts` and adapts — it has no knowledge of the host product:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
// mfe-capacidad-1/src/View.tsx
|
|
103
|
+
import type { Capacidad1Props } from '@org/contracts';
|
|
104
|
+
export default function View({ mode, showExtraContext, onAction }: Capacidad1Props) {
|
|
105
|
+
// same component, behavior adapts to `mode`
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Dynamic, license-gated loading (the differentiator)
|
|
110
|
+
|
|
111
|
+
On boot the shell asks the backend which remotes this tenant is licensed for, then registers them with the MF 2.0 runtime. Unlicensed bundles are **never downloaded**.
|
|
112
|
+
|
|
113
|
+
```
|
|
114
|
+
GET /api/mf-manifest?tenant=cliente-123
|
|
115
|
+
{
|
|
116
|
+
"products": ["producto-a", "producto-c"],
|
|
117
|
+
"features": ["capacidad-1", "capacidad-2"],
|
|
118
|
+
"remotes": {
|
|
119
|
+
"mfe_capacidad_1": "https://cdn.org.com/cap1/2.4.1/mf-manifest.json",
|
|
120
|
+
"mfe_capacidad_2": "https://cdn.org.com/cap2/1.8.0/mf-manifest.json"
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// shell/src/bootstrap.ts
|
|
127
|
+
import { init, registerRemotes } from '@module-federation/enhanced/runtime';
|
|
128
|
+
|
|
129
|
+
const manifest = await fetch(`/api/mf-manifest?tenant=${tenantId}`).then(r => r.json());
|
|
130
|
+
|
|
131
|
+
init({
|
|
132
|
+
name: 'shell',
|
|
133
|
+
remotes: [],
|
|
134
|
+
shared: {
|
|
135
|
+
react: { singleton: true, requiredVersion: '^19.0.0' },
|
|
136
|
+
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
registerRemotes(
|
|
141
|
+
Object.entries(manifest.remotes).map(([name, entry]) => ({ name, entry })),
|
|
142
|
+
);
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Commercial implication:** selling a new module to a tenant = flipping a backend flag. The next session's manifest includes the remote and the UI exposes it. **No redeploy, no client update.**
|
|
146
|
+
|
|
147
|
+
## Static shell config (build-time remotes that are always present)
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// shell/vite.config.ts
|
|
151
|
+
import { federation } from '@module-federation/vite';
|
|
152
|
+
export default defineConfig({
|
|
153
|
+
plugins: [react(), federation({
|
|
154
|
+
name: 'shell',
|
|
155
|
+
remotes: {
|
|
156
|
+
mfe_capacidad_1: 'mfe_capacidad_1@/remotes/cap1/mf-manifest.json',
|
|
157
|
+
},
|
|
158
|
+
shared: {
|
|
159
|
+
react: { singleton: true, requiredVersion: '^19.0.0' },
|
|
160
|
+
'react-dom': { singleton: true, requiredVersion: '^19.0.0' },
|
|
161
|
+
'@tanstack/react-router': { singleton: true },
|
|
162
|
+
'@tanstack/react-query': { singleton: true },
|
|
163
|
+
'@org/ui-kit': { singleton: true },
|
|
164
|
+
'@org/contracts': { singleton: true },
|
|
165
|
+
},
|
|
166
|
+
})],
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Remote config exposes its view and **mirrors** the `shared` block (must be symmetric, or dependencies duplicate at runtime):
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// mfe-capacidad-1/vite.config.ts
|
|
174
|
+
federation({
|
|
175
|
+
name: 'mfe_capacidad_1',
|
|
176
|
+
filename: 'remoteEntry.js',
|
|
177
|
+
exposes: { './View': './src/View.tsx' },
|
|
178
|
+
shared: { /* same singleton block as the shell */ },
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## Contract typing & version-skew safety
|
|
183
|
+
|
|
184
|
+
Module Federation 2.0 can download a remote's **TypeScript types at build time** (`dts`). Treat exposed modules as **public APIs**: add optional props with defaults, never rename/remove props consumers rely on. If the host downloads remote types, a breaking contract change becomes a **build error** instead of a runtime crash.
|
|
185
|
+
|
|
186
|
+
- **MUST** run contract tests in CI between shell and remotes.
|
|
187
|
+
- **SHOULD** deprecate props with a window before removal; bump `@org/contracts` major on breaking change.
|
|
188
|
+
|
|
189
|
+
## Risks & mitigations
|
|
190
|
+
|
|
191
|
+
| Risk | Mitigation |
|
|
192
|
+
|---|---|
|
|
193
|
+
| Version skew between shell and a remote | Semver `@org/contracts`; MF 2.0 `dts` build-time types; contract tests in CI |
|
|
194
|
+
| Duplicate React/router instances | `singleton: true` + `requiredVersion` on both sides; symmetric `shared` |
|
|
195
|
+
| CDN serving remotes goes down | Service Worker caching + offline fallback in `ErrorBoundary` |
|
|
196
|
+
| CSS collisions between MFEs | CSS Modules / per-MFE prefix; shared design tokens via `@org/ui-kit` |
|
|
197
|
+
| Uncontrolled `ui-kit` growth | Allow duplication first; promote to the kit only after a pattern is validated in 2+ MFEs |
|
|
198
|
+
|
|
199
|
+
## The checkpoint that proves the design
|
|
200
|
+
|
|
201
|
+
The critical moment is introducing the **second product**: if it requires modifying any capability MFE, the prop contract is wrong and must be fixed before continuing. A well-designed contract makes the 20th product as easy as the first.
|
|
202
|
+
|
|
203
|
+
## Metrics of success
|
|
204
|
+
|
|
205
|
+
- **Zero functional duplication** — a business rule changes in exactly one place.
|
|
206
|
+
- **Bundle proportional to license** — a single-product tenant never downloads the full catalog.
|
|
207
|
+
- **Independent deploy** — a capability fix reaches production without redeploying the others (< 15 min).
|
|
208
|
+
- **License activation without redeploy** — a new module is live in the next session.
|
|
209
|
+
- **INP < 100ms** on critical interactions; resilient during CDN/connectivity loss.
|
|
210
|
+
|
|
211
|
+
## References
|
|
212
|
+
|
|
213
|
+
- Module Federation 2.0 — https://module-federation.io/blog/announcement.html
|
|
214
|
+
- Runtime API (`init`, `registerRemotes`, `loadRemote`) — https://module-federation.io/guide/runtime/runtime-api
|
|
215
|
+
- `@module-federation/vite` — https://module-federation.io/
|
|
216
|
+
- TanStack Router — https://tanstack.com/router
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Single-SPA Microfrontends Standard
|
|
3
|
+
platform: web
|
|
4
|
+
track: single-spa
|
|
5
|
+
load_when: "Building a web app composed of independently-deployed microfrontends that may use DIFFERENT frameworks, or that require hard runtime isolation (CSS/JS/lifecycle) per module."
|
|
6
|
+
updated: 2026-06
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Single-SPA Microfrontends Standard
|
|
10
|
+
|
|
11
|
+
> **Choose this track when** you need to compose microfrontends that are **independently deployed** and possibly built with **different frameworks/versions** (React + Angular + legacy), or when each module must have a **fully isolated lifecycle** (its own bootstrap/mount/unmount, CSS injected and removed on navigation). If your whole frontend is a single homogeneous React stack, prefer the [Module Federation track](../microfrontends/module-federation-standard.md) instead — it shares dependencies more efficiently and gives end-to-end typing.
|
|
12
|
+
|
|
13
|
+
Single-SPA is a **top-level router/orchestrator**: a thin shell registers applications and decides which one is active for a given route. Each microfrontend exposes `bootstrap`/`mount`/`unmount` lifecycles.
|
|
14
|
+
|
|
15
|
+
## Core rules
|
|
16
|
+
|
|
17
|
+
- **MUST** keep the shell free of business logic. The shell only registers apps, owns global providers (auth, i18n, event bus) and passes them as `customProps`.
|
|
18
|
+
- **MUST** expose `bootstrap`, `mount`, `unmount` lifecycles from each MFE entry (`src/spa.tsx`).
|
|
19
|
+
- **MUST** set `domElementGetter` so the MFE mounts inside the shell container, not at the end of `<body>`.
|
|
20
|
+
- **MUST** isolate CSS per MFE via `cssLifecycleFactory` (injected on mount, removed on unmount).
|
|
21
|
+
- **MUST** wrap each MFE in an error boundary so one module failing never takes down the shell.
|
|
22
|
+
- **MUST** give every MFE a unique route prefix matching its `base` (e.g. `/finance/*`).
|
|
23
|
+
- **SHOULD** ship a standalone entry (`src/main.tsx`) so each MFE runs in isolation during development.
|
|
24
|
+
- **SHOULD** share singletons (`react`, `react-dom`, router, query client) via an SystemJS import map to avoid duplicate instances.
|
|
25
|
+
|
|
26
|
+
## Stack (2026)
|
|
27
|
+
|
|
28
|
+
| Concern | Choice |
|
|
29
|
+
|---|---|
|
|
30
|
+
| Orchestrator | `single-spa` 6+ |
|
|
31
|
+
| React adapter | `single-spa-react` |
|
|
32
|
+
| Bundler plugin | `vite-plugin-single-spa` |
|
|
33
|
+
| Framework | React 19 (18 still supported) + TypeScript strict |
|
|
34
|
+
| Router (per MFE) | TanStack Router 1+ |
|
|
35
|
+
| Module loader | SystemJS 6 + import maps |
|
|
36
|
+
|
|
37
|
+
## MFE entry (`src/spa.tsx`)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import React from 'react';
|
|
41
|
+
import ReactDOMClient from 'react-dom/client';
|
|
42
|
+
import singleSpaReact from 'single-spa-react';
|
|
43
|
+
import { cssLifecycleFactory } from 'vite-plugin-single-spa/ex';
|
|
44
|
+
import App from './App';
|
|
45
|
+
|
|
46
|
+
// Context the shell injects into every MFE.
|
|
47
|
+
export interface ShellProps {
|
|
48
|
+
i18n?: unknown;
|
|
49
|
+
eventBus?: unknown;
|
|
50
|
+
authContext?: unknown;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function ErrorFallback({ error }: { error: Error }) {
|
|
54
|
+
return (
|
|
55
|
+
<div className="flex min-h-screen items-center justify-center bg-red-50">
|
|
56
|
+
<div className="max-w-md rounded-lg bg-white p-6 shadow-lg">
|
|
57
|
+
<h2 className="mb-2 text-xl font-semibold text-red-600">Error en el módulo</h2>
|
|
58
|
+
<p className="mb-4 text-slate-600">{error.message}</p>
|
|
59
|
+
<button
|
|
60
|
+
onClick={() => window.location.reload()}
|
|
61
|
+
className="rounded bg-red-600 px-4 py-2 text-white hover:bg-red-700"
|
|
62
|
+
>
|
|
63
|
+
Recargar
|
|
64
|
+
</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const lifecycles = singleSpaReact({
|
|
71
|
+
React,
|
|
72
|
+
ReactDOMClient,
|
|
73
|
+
rootComponent: App,
|
|
74
|
+
errorBoundary: (err: Error) => <ErrorFallback error={err} />,
|
|
75
|
+
// CRITICAL: mount inside the shell container, not at the end of <body>.
|
|
76
|
+
domElementGetter: () => document.getElementById('single-spa-application')!,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const cssLc = cssLifecycleFactory('spa');
|
|
80
|
+
|
|
81
|
+
export const bootstrap = [cssLc.bootstrap, lifecycles.bootstrap];
|
|
82
|
+
export const mount = [cssLc.mount, lifecycles.mount];
|
|
83
|
+
export const unmount = [cssLc.unmount, lifecycles.unmount];
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
> **Why `domElementGetter` is mandatory:** without it, `single-spa-react` creates a fresh `<div>` at the end of `<body>` and the MFE renders *outside* the shell layout — mounted correctly but invisible to the user. The shell must expose `<div id="single-spa-application">` in its layout.
|
|
87
|
+
|
|
88
|
+
## MFE Vite config (`vite.config.ts`)
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { defineConfig } from 'vite';
|
|
92
|
+
import react from '@vitejs/plugin-react';
|
|
93
|
+
import vitePluginSingleSpa from 'vite-plugin-single-spa';
|
|
94
|
+
|
|
95
|
+
export default defineConfig({
|
|
96
|
+
plugins: [
|
|
97
|
+
react(),
|
|
98
|
+
vitePluginSingleSpa({
|
|
99
|
+
serverPort: 3001, // unique per module
|
|
100
|
+
spaEntryPoints: 'src/spa.tsx',
|
|
101
|
+
cssStrategy: 'singleMife', // CSS injected/removed on mount/unmount
|
|
102
|
+
projectId: 'finance-module',
|
|
103
|
+
}),
|
|
104
|
+
],
|
|
105
|
+
server: { port: 3001, cors: true },
|
|
106
|
+
base: '/finance/', // route prefix
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Shell registration (`app-shell/src/microfrontends/register.ts`)
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { registerApplication, start } from 'single-spa';
|
|
114
|
+
|
|
115
|
+
const shared = {
|
|
116
|
+
i18n: i18nInstance,
|
|
117
|
+
eventBus,
|
|
118
|
+
authContext: authStore.getState(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
registerApplication({
|
|
122
|
+
name: 'finance',
|
|
123
|
+
app: () => System.import('http://localhost:3001/finance/spa.js'),
|
|
124
|
+
activeWhen: ['/finance'],
|
|
125
|
+
customProps: shared,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
registerApplication({
|
|
129
|
+
name: 'inventory',
|
|
130
|
+
app: () => System.import('http://localhost:3002/inventory/spa.js'),
|
|
131
|
+
activeWhen: ['/inventory'],
|
|
132
|
+
customProps: shared,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
start();
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Shell import map (`app-shell/index.html`) — pins shared singletons:
|
|
139
|
+
|
|
140
|
+
```html
|
|
141
|
+
<script type="systemjs-importmap">
|
|
142
|
+
{
|
|
143
|
+
"imports": {
|
|
144
|
+
"react": "https://cdn.jsdelivr.net/npm/react@19/umd/react.production.min.js",
|
|
145
|
+
"react-dom": "https://cdn.jsdelivr.net/npm/react-dom@19/umd/react-dom.production.min.js",
|
|
146
|
+
"single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6/lib/system/single-spa.min.js"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</script>
|
|
150
|
+
<script src="https://cdn.jsdelivr.net/npm/systemjs@6/dist/system.min.js"></script>
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Port convention
|
|
154
|
+
|
|
155
|
+
| Module | Port | Role |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| app-shell | 3000 | Orchestrator |
|
|
158
|
+
| finance | 3001 | MFE |
|
|
159
|
+
| inventory | 3002 | MFE |
|
|
160
|
+
| hr | 3003 | MFE |
|
|
161
|
+
| crm | 3004 | MFE |
|
|
162
|
+
|
|
163
|
+
## Per-MFE routing (TanStack Router)
|
|
164
|
+
|
|
165
|
+
Every route MUST carry the module prefix that matches `base`:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const dashboardRoute = createRoute({
|
|
169
|
+
getParentRoute: () => rootRoute,
|
|
170
|
+
path: '/finance/dashboard',
|
|
171
|
+
component: Dashboard,
|
|
172
|
+
});
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Checklist
|
|
176
|
+
|
|
177
|
+
**MFE**
|
|
178
|
+
- [ ] `vite-plugin-single-spa` + `single-spa-react` installed
|
|
179
|
+
- [ ] `src/spa.tsx` exports `bootstrap`/`mount`/`unmount`
|
|
180
|
+
- [ ] `cssLifecycleFactory` wired for CSS isolation
|
|
181
|
+
- [ ] `errorBoundary` implemented
|
|
182
|
+
- [ ] **`domElementGetter` → `#single-spa-application`** ⚠️
|
|
183
|
+
- [ ] Unique port + `base` prefix; all routes use the prefix
|
|
184
|
+
- [ ] `cors: true`; standalone `src/main.tsx` for dev
|
|
185
|
+
|
|
186
|
+
**Shell**
|
|
187
|
+
- [ ] `single-spa` installed; import map with shared singletons
|
|
188
|
+
- [ ] `registerApplication` per MFE + `start()`
|
|
189
|
+
- [ ] `customProps` with i18n/auth/eventBus
|
|
190
|
+
- [ ] `<div id="single-spa-application">` present in layout ⚠️
|
|
191
|
+
|
|
192
|
+
## References
|
|
193
|
+
|
|
194
|
+
- Single-SPA — https://single-spa.js.org/
|
|
195
|
+
- single-spa-react — https://single-spa.js.org/docs/ecosystem-react/
|
|
196
|
+
- vite-plugin-single-spa — https://github.com/single-spa/vite-plugin-single-spa
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Single-Page Application (SPA) Standard
|
|
3
|
+
platform: web
|
|
4
|
+
track: spa
|
|
5
|
+
load_when: "Building one cohesive web application that ships as a single deployable — no microfrontends, no runtime composition."
|
|
6
|
+
updated: 2026-06
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Single-Page Application (SPA) Standard
|
|
10
|
+
|
|
11
|
+
> **Choose this track** for a single, cohesive app deployed as one unit. It's the default for most products. Only move to [Single-SPA](../single-spa/single-spa-standard.md) or [Module Federation](../microfrontends/module-federation-standard.md) when you have a real need for independently-deployed modules — don't adopt microfrontend complexity speculatively.
|
|
12
|
+
|
|
13
|
+
A monolithic SPA still follows the same Clean Architecture + DDD folder structure and conventions as every web track. See [`frontend-architecture.md`](../_base/frontend-architecture.md) and [`frontend-standards.md`](../_base/frontend-standards.md) for the shared rules; this document only states what is specific to the single-deployable case.
|
|
14
|
+
|
|
15
|
+
## Core rules
|
|
16
|
+
|
|
17
|
+
- **MUST** ship as a single Vite build; no `remoteEntry`/federation, no SystemJS import maps.
|
|
18
|
+
- **MUST** use TanStack Router file-based routing with pathless layouts for auth/app boundaries.
|
|
19
|
+
- **MUST** code-split per route (automatic with TanStack Router) to keep the initial bundle < 500KB gzipped.
|
|
20
|
+
- **SHOULD** organize by business module/domain/feature, not by technical layer.
|
|
21
|
+
- **SHOULD** lazy-load heavy, rarely-used routes and features.
|
|
22
|
+
|
|
23
|
+
## Vite config (`vite.config.ts`)
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineConfig } from 'vite';
|
|
27
|
+
import react from '@vitejs/plugin-react';
|
|
28
|
+
import { tanstackRouter } from '@tanstack/router-plugin/vite';
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
plugins: [
|
|
32
|
+
tanstackRouter({ target: 'react', autoCodeSplitting: true }),
|
|
33
|
+
react(),
|
|
34
|
+
],
|
|
35
|
+
build: { target: 'esnext' },
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## When to graduate to microfrontends
|
|
40
|
+
|
|
41
|
+
Adopt a microfrontend track only when **at least one** is true:
|
|
42
|
+
|
|
43
|
+
- Independent teams need to deploy modules on **separate release cadences**.
|
|
44
|
+
- A capability is **reused across multiple products** with different layouts.
|
|
45
|
+
- You need to **enable/disable modules per tenant/license at runtime** (→ Module Federation).
|
|
46
|
+
- You must integrate modules built with **different frameworks** (→ Single-SPA).
|
|
47
|
+
|
|
48
|
+
Until then, a well-structured SPA is faster to build, debug and deploy.
|
|
49
|
+
|
|
50
|
+
## References
|
|
51
|
+
|
|
52
|
+
- TanStack Router — https://tanstack.com/router
|
|
53
|
+
- Vite — https://vite.dev/
|