Compare commits
10 Commits
bca04f4cef
...
6a5d602b3b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a5d602b3b | ||
|
|
8bc32095c1 | ||
|
|
ee45bfe932 | ||
|
|
7ef0411ffa | ||
|
|
9cfa54d382 | ||
|
|
15d603b00c | ||
|
|
d71e30bb3a | ||
|
|
e734530ffe | ||
|
|
a28de6884c | ||
|
|
3ce71447b3 |
22
AGENTS.md
22
AGENTS.md
@@ -308,13 +308,31 @@ Each feature package exposes exactly these subpath exports:
|
||||
| Subpath | What it exports | Who consumes |
|
||||
| ---------------------- | -------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||
| `.` (root) | Contracts only: types, errors, schemas, `IUseCase` / `IController` aliases, router type, constants | Any consumer |
|
||||
| `./ui` | Query builders (`queryOptions`), UI components | App packages |
|
||||
| `./ui` | Hooks (`useX`), components, query builders (`queryOptions`) | App packages |
|
||||
| `./api` | tRPC router (`xRouter` + `XRouter` type) | `@repo/core-api` only |
|
||||
| `./cms` | Payload collections | `@repo/core-cms` only |
|
||||
| `./di/bind-production` | App boot side-effect — swaps mock for real Payload impl | App packages only |
|
||||
| `./di/bind-dev-seed` | App boot side-effect — swaps empty mock for populated mock | App packages, storybook |
|
||||
|
||||
Apps import schemas/types from `@repo/<feature>` (root) and React Query builders from `@repo/<feature>/ui`. Deep source paths are not accessible — the `exports` map enforces this.
|
||||
Apps import schemas/types from `@repo/<feature>` (root) and hooks/components from `@repo/<feature>/ui`. Deep source paths are not accessible — the `exports` map enforces this.
|
||||
|
||||
### Feature UI structure
|
||||
|
||||
Each feature's `src/ui/` follows this layout:
|
||||
|
||||
```
|
||||
src/ui/
|
||||
index.ts # Barrel — exports server components as public API
|
||||
query.ts # Query builder functions (framework-agnostic)
|
||||
hooks/
|
||||
use-<entity>.ts # "use client" — wraps useTRPC + useSuspenseQuery
|
||||
components/
|
||||
<entity>-list.server.tsx # Server — DI + prefetch + HydrationBoundary (public)
|
||||
<entity>-list.client.tsx # "use client" — calls hook (internal only)
|
||||
<entity>-card.tsx # Presentational (receives props)
|
||||
```
|
||||
|
||||
Server components (`.server.tsx`) are the public API — the barrel exports them under clean names (`ArticleList`, not `ArticleListServer`). Client components (`.client.tsx`) are internal — only imported by their `.server` counterpart. Server components resolve controllers from DI, prefetch data, and wrap client components in `HydrationBoundary` for SSR + instant hydration. App pages just import and render: `<ArticleList />`, `<PageContent slug="about" />`. See [`docs/guides/building-feature-ui.md`](./docs/guides/building-feature-ui.md) for the full guide.
|
||||
|
||||
### Payload-backed features use constructor injection
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ Turborepo + pnpm monorepo organized by vertical features. Each feature (`auth`,
|
||||
- `docs/guides/coverage.md` — 4-layer coverage cookbook (L0 vitest thresholds, L1 `pnpm coverage:diff`, L2 aggregate, L3 mutation; ADR-020)
|
||||
- `docs/guides/releasing.md` — release-please workflow: how Conventional Commits become tagged versions + per-package CHANGELOGs (ADR-021)
|
||||
- `docs/architecture/template-tiers.md` — must-have vs optional packages and how to scaffold the optionals
|
||||
- `docs/guides/building-feature-ui.md` — Feature UI components, hooks, data fetching (tRPC + React Query), SSR prefetch/hydration, seed data, DI wiring
|
||||
- `docs/guides/compliance-overview.md` — hub for operator compliance obligations: GDPR, cookie consent, DSR, and pre-launch checklist
|
||||
|
||||
## Conformance system
|
||||
@@ -105,7 +106,8 @@ See `docs/guides/coverage.md` for the cookbook and ADR-020 for the full rational
|
||||
- **Schemas in the use-case file** — Every use case exports `xInputSchema` (a `z.ZodObject` with `.strict()`; `z.object({}).strict()` for void inputs) and, for non-void use cases, `xOutputSchema`. Types: `XInput = z.infer<typeof xInputSchema>` and `XOutput`. Use case body ends with `xOutputSchema.parse(result)` before returning (runtime guarantee against malformed repository data)
|
||||
- **Controllers receive `unknown` + presenter** — Controllers `safeParse(xInputSchema)` from the use-case file and throw `InputParseError` on failure. Non-void controllers define a top-level `function presenter(value: XOutput)` and return `Promise<ReturnType<typeof presenter>>` (identity is fine — `return value`); void controllers return `Promise<void>` with no presenter
|
||||
- **Feature-scoped tRPC error mapping** — Each feature has `integrations/api/procedures.ts` exporting `xProcedure = t.procedure.use(defineErrorMiddleware([[Ctor, "TRPC_CODE"], ...]))` from `@repo/core-shared/trpc/define-error-middleware`. Routers use `xProcedure.input(xInputSchema)` — schemas are imported from the use-case file, never redefined inline. `core-shared` never enumerates feature error classes
|
||||
- **Public surface split** — Feature root (`.`) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (query builders, components) live behind `./ui` (`src/ui/index.ts`). Apps import queries from `@repo/<feature>/ui`, schemas/types from `@repo/<feature>`
|
||||
- **Public surface split** — Feature root (`.`) exports contracts only: types, errors, schemas, IUseCase / IController aliases, router type, constants. UI artifacts (hooks, components, query builders) live behind `./ui` (`src/ui/index.ts`). Apps import hooks/components from `@repo/<feature>/ui`, schemas/types from `@repo/<feature>`
|
||||
- **Feature UI owns its data fetching** — Each feature's `src/ui/hooks/` contains `"use client"` hooks that wrap `useTRPC` + `useSuspenseQuery`. Connected components in `src/ui/components/` call these hooks. App pages prefetch via `appRouter.createCaller({})` and hydrate via `HydrationBoundary` + `dehydrate` + `setQueryData`. See `docs/guides/building-feature-ui.md`
|
||||
- **Payload repositories via constructor** — Feature packages receive Payload config at constructor time, not as a direct dependency
|
||||
- **Three binding modes per feature** — Each feature exports two binders: `./di/bind-production` (real Payload) and `./di/bind-dev-seed` (populated mock). The app's `bindAll()` dispatcher in `apps/web-next/src/server/bind-production.ts` picks one by env: `USE_DEV_SEED="true"` → dev seed; `NODE_ENV="production"` → production; otherwise → dev seed (developer default so `pnpm dev` boots without Payload). Dev seed lives in `src/__seeds__/dev.ts` as a lazy `buildDev<Entities>()` function that uses the feature's existing factory
|
||||
- **Binders take a `ctx` arg from `core-shared/di`** — `bindProductionX(ctx: BindProductionContext)` for production binders; `bindDevSeedX(ctx: BindContext)` for dev-seed. Required fields: `tracer`, `logger`, plus `config` for production. Optional fields: `bus`, `queue`, `realtime`, `realtimeRegistry` (correspond to optional core packages — guard with `?.` or `if (bus) { ... }` when used; use-case signatures should accept the protocol type when they only need protocol methods, not the full concrete interface). Aggregator builds one ctx object and passes it to all feature binders
|
||||
|
||||
@@ -16,6 +16,7 @@ const nextConfig = {
|
||||
"@repo/marketing-pages",
|
||||
"@repo/media",
|
||||
"@repo/navigation",
|
||||
"@repo/core-trpc",
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -19,19 +19,22 @@
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@repo/core-cms": "workspace:*",
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@repo/core-trpc": "workspace:^",
|
||||
"@repo/marketing-pages": "workspace:*",
|
||||
"@repo/media": "workspace:*",
|
||||
"@repo/navigation": "workspace:*",
|
||||
"@sentry/nextjs": "^10.51.0",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"@tailwindcss/postcss": "^4.3.0",
|
||||
"@tanstack/react-query": "^5.96.2",
|
||||
"@trpc/server": "^11.17.0",
|
||||
"inversify": "^6.2.0",
|
||||
"next": "^15.3.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"superjson": "^2.2.1"
|
||||
"superjson": "^2.2.1",
|
||||
"tailwindcss": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.50.0",
|
||||
|
||||
5
apps/web-next/postcss.config.mjs
Normal file
5
apps/web-next/postcss.config.mjs
Normal file
@@ -0,0 +1,5 @@
|
||||
export default {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
@@ -1,31 +1,12 @@
|
||||
import { appRouter } from "@repo/core-api";
|
||||
import { PageContent } from "@repo/marketing-pages/ui";
|
||||
import { bindAll } from "../../server/bind-production";
|
||||
|
||||
export default async function AboutPage() {
|
||||
await bindAll();
|
||||
const caller = appRouter.createCaller({});
|
||||
const page = await caller.marketingPages.pageBySlug({ slug: "about" });
|
||||
|
||||
if (!page) {
|
||||
return (
|
||||
<main>
|
||||
<h1>About</h1>
|
||||
<p>This page hasn't been published yet.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{page.hero.heading}</h1>
|
||||
{page.hero.subheading ? <p>{page.hero.subheading}</p> : null}
|
||||
</header>
|
||||
<pre style={{ whiteSpace: "pre-wrap" }}>
|
||||
{JSON.stringify(page.layout, null, 2)}
|
||||
</pre>
|
||||
</article>
|
||||
<main className="px-6 py-8">
|
||||
<PageContent slug="about" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
13
apps/web-next/src/app/api/trpc/[trpc]/route.ts
Normal file
13
apps/web-next/src/app/api/trpc/[trpc]/route.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter } from "@repo/core-api";
|
||||
|
||||
const handler = async (req: Request) => {
|
||||
return fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: () => ({}),
|
||||
});
|
||||
};
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
@@ -1,5 +1,4 @@
|
||||
import { notFound } from "next/navigation";
|
||||
import { appRouter } from "@repo/core-api";
|
||||
import { ArticleDetail } from "@repo/blog/ui";
|
||||
import { bindAll } from "../../../server/bind-production";
|
||||
|
||||
type PageProps = {
|
||||
@@ -9,26 +8,10 @@ type PageProps = {
|
||||
export default async function BlogPostPage({ params }: PageProps) {
|
||||
await bindAll();
|
||||
const { slug } = await params;
|
||||
const caller = appRouter.createCaller({});
|
||||
const article = await caller.blog.articleBySlug({ slug });
|
||||
|
||||
if (!article) notFound();
|
||||
|
||||
return (
|
||||
<main>
|
||||
<article>
|
||||
<header>
|
||||
<h1>{article.title}</h1>
|
||||
{article.createdAt ? (
|
||||
<time dateTime={article.createdAt.toISOString()}>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
) : null}
|
||||
</header>
|
||||
<pre style={{ whiteSpace: "pre-wrap" }}>
|
||||
{JSON.stringify(article.content, null, 2)}
|
||||
</pre>
|
||||
</article>
|
||||
<main className="px-6 py-8">
|
||||
<ArticleDetail slug={slug} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Metadata } from "next";
|
||||
import "../styles/app.css";
|
||||
import { getNonce } from "@repo/core-shared/security/next";
|
||||
import { bindAll } from "../server/bind-production";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -12,6 +14,7 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
await bindAll();
|
||||
const nonce = await getNonce();
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,49 +1,15 @@
|
||||
import Link from "next/link";
|
||||
import { appRouter } from "@repo/core-api";
|
||||
import { ArticleList } from "@repo/blog/ui";
|
||||
import { bindAll } from "../server/bind-production";
|
||||
|
||||
export default async function Home() {
|
||||
await bindAll();
|
||||
const caller = appRouter.createCaller({});
|
||||
|
||||
const [siteSettings, header, articles] = await Promise.all([
|
||||
caller.marketingPages.siteSettings({}),
|
||||
caller.navigation.header({}),
|
||||
caller.blog.listArticles({ status: "published", limit: 20 }),
|
||||
]);
|
||||
|
||||
return (
|
||||
<main>
|
||||
<header>
|
||||
<h1>{siteSettings.siteName}</h1>
|
||||
{siteSettings.siteDescription ? (
|
||||
<p>{siteSettings.siteDescription}</p>
|
||||
) : null}
|
||||
<nav>
|
||||
<ul>
|
||||
{header.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<section>
|
||||
<h2>Latest articles</h2>
|
||||
{articles.length === 0 ? (
|
||||
<p>No published articles yet.</p>
|
||||
) : (
|
||||
<ul>
|
||||
{articles.map((a) => (
|
||||
<li key={a.id}>
|
||||
<Link href={`/blog/${a.slug}`}>{a.title}</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
<main className="mx-auto max-w-5xl px-6 py-8">
|
||||
<h2 className="mb-4 text-2xl font-bold text-foreground">
|
||||
Latest articles
|
||||
</h2>
|
||||
<ArticleList />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { NextTrpcProvider } from "@repo/core-trpc/next";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
return <NextTrpcProvider>{children}</NextTrpcProvider>;
|
||||
}
|
||||
|
||||
1
apps/web-next/src/css.d.ts
vendored
Normal file
1
apps/web-next/src/css.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
@@ -28,7 +28,7 @@ import { bindDevSeedMarketingPages } from "@repo/marketing-pages/di/bind-dev-see
|
||||
import { bindDevSeedNavigation } from "@repo/navigation/di/bind-dev-seed";
|
||||
import { bindDevSeedMedia } from "@repo/media/di/bind-dev-seed";
|
||||
|
||||
let bound = false;
|
||||
let bindPromise: Promise<void> | null = null;
|
||||
|
||||
// Shared container holds TRACER + LOGGER bindings; per-feature containers
|
||||
// receive references via parameter passing. This separates the instrumentation
|
||||
@@ -87,8 +87,6 @@ function resolveJobsDevSeed(): { queue: IJobQueue } {
|
||||
* feature via `bindProductionX` exports.
|
||||
*/
|
||||
export async function bindAllProduction(): Promise<void> {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
const { tracer, logger } = resolveInstrumentation(); // Rule 0
|
||||
const { queue } = await resolveJobsProduction();
|
||||
const resolvedConfig = await config;
|
||||
@@ -114,8 +112,6 @@ export async function bindAllProduction(): Promise<void> {
|
||||
* Payload booted. Mutually exclusive with `bindAllProduction()`.
|
||||
*/
|
||||
export async function bindAllDevSeed(): Promise<void> {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
const { tracer, logger } = resolveInstrumentation(); // Rule 0
|
||||
const { queue } = resolveJobsDevSeed();
|
||||
|
||||
@@ -150,21 +146,25 @@ export async function bindAllDevSeed(): Promise<void> {
|
||||
* When @repo/core-realtime is scaffolded, extend to accept realtime deps
|
||||
* (IRealtimeBroadcaster, IRealtimeHandlerRegistry) and pass them through.
|
||||
*/
|
||||
export async function bindAll(): Promise<void> {
|
||||
if (process.env.USE_DEV_SEED === "true") {
|
||||
await bindAllDevSeed();
|
||||
return;
|
||||
export function bindAll(): Promise<void> {
|
||||
if (bindPromise) return bindPromise;
|
||||
|
||||
if (process.env.USE_DEV_SEED === "false") {
|
||||
bindPromise = bindAllProduction();
|
||||
} else if (process.env.USE_DEV_SEED === "true") {
|
||||
bindPromise = bindAllDevSeed();
|
||||
} else if (process.env.NODE_ENV === "production") {
|
||||
bindPromise = bindAllProduction();
|
||||
} else {
|
||||
bindPromise = bindAllDevSeed();
|
||||
}
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
await bindAllProduction();
|
||||
return;
|
||||
}
|
||||
await bindAllDevSeed();
|
||||
|
||||
return bindPromise;
|
||||
}
|
||||
|
||||
/** Test-only resets — not exported via package. Used by bind-production.test.ts. */
|
||||
export function __resetBindStateForTests(): void {
|
||||
bound = false;
|
||||
bindPromise = null;
|
||||
resolvedTracer = null;
|
||||
resolvedLogger = null;
|
||||
resolvedQueue = null;
|
||||
|
||||
10
apps/web-next/src/styles/app.css
Normal file
10
apps/web-next/src/styles/app.css
Normal file
@@ -0,0 +1,10 @@
|
||||
@import "tailwindcss";
|
||||
@source "../../../../packages/core-ui/src";
|
||||
@source "../../../../packages/navigation/src";
|
||||
@source "../../../../packages/blog/src";
|
||||
@source "../../../../packages/marketing-pages/src";
|
||||
@source "../../../../packages/media/src";
|
||||
@source "../../../../packages/auth/src";
|
||||
@source "../";
|
||||
|
||||
@import "../../../../packages/core-ui/src/styles/theme.css";
|
||||
498
docs/guides/building-feature-ui.md
Normal file
498
docs/guides/building-feature-ui.md
Normal file
@@ -0,0 +1,498 @@
|
||||
# Building Feature UI — Components, Hooks & Data Fetching
|
||||
|
||||
Each feature owns its UI layer inside `src/ui/`. This guide covers how to
|
||||
create React components that fetch their own data via tRPC + React Query,
|
||||
how to wire them into Next.js and TanStack Start apps, and how the server
|
||||
prefetch + client hydration pattern works.
|
||||
|
||||
> **Prerequisites:** `@repo/core-trpc` must be scaffolded
|
||||
> (`pnpm turbo gen core-package trpc`). The tRPC providers must be wired
|
||||
> into the app's root layout (see [App wiring](#app-wiring) below).
|
||||
|
||||
---
|
||||
|
||||
## Feature `src/ui/` folder structure
|
||||
|
||||
```
|
||||
packages/<feature>/src/ui/
|
||||
index.ts # Barrel — re-exports server components as public API
|
||||
query.ts # Query builder functions (framework-agnostic)
|
||||
hooks/
|
||||
use-<entity>.ts # "use client" hooks wrapping tRPC + useSuspenseQuery
|
||||
use-<entity>-list.ts
|
||||
components/
|
||||
<entity>-card.tsx # Presentational (receives props, no hooks)
|
||||
<entity>-list.server.tsx # Server component — DI + prefetch + HydrationBoundary
|
||||
<entity>-list.client.tsx # "use client" — calls hook, owns rendering
|
||||
<entity>-detail.server.tsx
|
||||
<entity>-detail.client.tsx
|
||||
```
|
||||
|
||||
### Naming convention
|
||||
|
||||
| File suffix | Directive | Role | Exported from barrel? |
|
||||
| ------------------ | ---------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| `.server.tsx` | _(none — server by default)_ | Resolves controller from DI, prefetches data, wraps `.client` in `HydrationBoundary` | **Yes** — under the clean name (e.g. `ArticleList`) |
|
||||
| `.client.tsx` | `"use client"` | Calls hooks, renders UI | **No** — internal to the feature; only imported by its `.server` counterpart |
|
||||
| `.tsx` (no suffix) | _(none)_ | Presentational — receives data via props, no hooks | Yes, if useful standalone (e.g. `ArticleCard`) |
|
||||
|
||||
The **server component is the public face** — the barrel exports it under
|
||||
the clean component name (`ArticleList`, `ArticleDetail`, `PageContent`).
|
||||
The `.client.tsx` suffix signals "internal, not for direct consumption" —
|
||||
consumers never see it.
|
||||
|
||||
### Component roles
|
||||
|
||||
- **Server components** (`.server.tsx`) — resolve the controller from the
|
||||
feature's DI container, call it to prefetch data, seed the React Query
|
||||
cache via `setQueryData`, and wrap the client component in
|
||||
`HydrationBoundary`. This gives SSR + instant hydration.
|
||||
- **Client components** (`.client.tsx`) — `"use client"`. Call hooks from
|
||||
`hooks/` to get data via `useSuspenseQuery`. Handle rendering + interactivity.
|
||||
Never imported by app pages directly.
|
||||
- **Hooks** (`hooks/`) — own data fetching; one hook per query.
|
||||
Always `"use client"`. Import `useTRPC` from `@repo/core-trpc` and
|
||||
`useSuspenseQuery` from `@tanstack/react-query`.
|
||||
- **Presentational components** (`.tsx`, no suffix) — receive data via props.
|
||||
No `"use client"` unless they need browser APIs. Can be shared by
|
||||
multiple client components.
|
||||
|
||||
---
|
||||
|
||||
## Component composition & `@repo/core-ui` reuse
|
||||
|
||||
Feature components **must** reuse primitives from `@repo/core-ui` rather
|
||||
than hand-rolling HTML with raw Tailwind classes. `core-ui` follows
|
||||
**Atomic Design**:
|
||||
|
||||
| Tier | Location | Examples | Rule |
|
||||
| ------------- | ------------------------ | ----------------------------------- | --------------------------------------------- |
|
||||
| **Atoms** | `core-ui/src/atoms/` | `Button`, `Input`, `Label` | Smallest building blocks. No business logic. |
|
||||
| **Molecules** | `core-ui/src/molecules/` | `FormField` (Label + Input + error) | Compose atoms. Still generic. |
|
||||
| **Organisms** | `core-ui/src/organisms/` | `CookieConsentBanner` | Compose molecules/atoms. May own local state. |
|
||||
| **Templates** | `core-ui/src/templates/` | Page shells, layout grids | Structural — define slots, no data. |
|
||||
|
||||
**Import direction is strictly upward:** atoms never import molecules;
|
||||
molecules never import organisms. The ESLint rule
|
||||
`atomic-tier-import-direction` enforces this.
|
||||
|
||||
### Where feature components fit
|
||||
|
||||
Feature components are **consumers** of core-ui, not replacements.
|
||||
They sit above the atomic tiers:
|
||||
|
||||
```
|
||||
App page (imports feature component, passes route props)
|
||||
└── Feature server component (.server.tsx — DI + prefetch + HydrationBoundary)
|
||||
└── Feature client component (.client.tsx — "use client", calls hook)
|
||||
└── Feature presentational component (receives props)
|
||||
└── core-ui atoms/molecules (Button, Input, FormField, ...)
|
||||
```
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- **Always check `core-ui` first.** Before creating a `<Card>` or
|
||||
`<Badge>` in a feature, check if `core-ui` already exports it. Use
|
||||
Storybook (`pnpm dev --filter @repo/storybook`) or the barrel at
|
||||
`packages/core-ui/src/index.ts`.
|
||||
- **If a primitive is missing, add it to `core-ui`** — not to the
|
||||
feature. Scaffold via `pnpm turbo gen core-ui-component`. Feature
|
||||
packages should not contain generic UI primitives.
|
||||
- **Feature components compose, not duplicate.** A feature's
|
||||
`<ArticleCard>` should render a `core-ui` `<Card>` (when it exists)
|
||||
with feature-specific content inside — not re-implement card styling.
|
||||
- **Tailwind utility classes are fine** for layout and spacing within
|
||||
feature components (flex, grid, padding, margin). But visual
|
||||
primitives (buttons, inputs, badges, cards) come from `core-ui`.
|
||||
|
||||
### Adding `core-ui` as a dependency
|
||||
|
||||
Feature packages that use core-ui atoms need:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
"dependencies": {
|
||||
"@repo/core-ui": "workspace:*"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create a hook
|
||||
|
||||
Hooks live in `src/ui/hooks/` and wrap a single tRPC query:
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/ui/hooks/use-article-list.ts
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export function useArticleList(options?: {
|
||||
status?: "draft" | "published";
|
||||
limit?: number;
|
||||
}) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(
|
||||
trpc.blog.listArticles.queryOptions({
|
||||
status: options?.status ?? "published",
|
||||
limit: options?.limit ?? 20,
|
||||
}),
|
||||
) as { data: Article[] };
|
||||
}
|
||||
```
|
||||
|
||||
> **TS2742 workaround:** Feature packages set `declaration: true` (from
|
||||
> the base tsconfig). The `as { data: T }` cast avoids a non-portable
|
||||
> return type error caused by `@trpc/client` resolving to different
|
||||
> `.pnpm` paths per package.
|
||||
|
||||
### Dependencies
|
||||
|
||||
Feature packages that have hooks need these dependencies:
|
||||
|
||||
```jsonc
|
||||
// package.json
|
||||
"dependencies": {
|
||||
"@repo/core-trpc": "workspace:*",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/client": "^11.17.0", // for type portability
|
||||
"react": "^19.0.0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create components
|
||||
|
||||
### Server component (public face)
|
||||
|
||||
The server component resolves the controller from DI, prefetches, and
|
||||
wraps the client component in `HydrationBoundary`:
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/ui/components/article-list.server.tsx
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { getQueryClient } from "@repo/core-trpc";
|
||||
import { blogContainer } from "../../di/container";
|
||||
import { BLOG_SYMBOLS } from "../../di/symbols";
|
||||
import type { IGetArticlesController } from "../../interface-adapters/controllers/get-articles.controller";
|
||||
import { ArticleList as ArticleListClient } from "./article-list.client";
|
||||
|
||||
export async function ArticleList() {
|
||||
const controller = blogContainer.get<IGetArticlesController>(
|
||||
BLOG_SYMBOLS.IGetArticlesController,
|
||||
);
|
||||
const articles = await controller({ status: "published", limit: 20 });
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(
|
||||
["blog", "listArticles", { input: { status: "published", limit: 20 } }],
|
||||
articles,
|
||||
);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticleListClient />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Client component (internal)
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/ui/components/article-list.client.tsx
|
||||
"use client";
|
||||
|
||||
import { useArticleList } from "../hooks/use-article-list";
|
||||
import { ArticleCard } from "./article-card";
|
||||
|
||||
export function ArticleList() {
|
||||
const { data: articles } = useArticleList();
|
||||
|
||||
if (articles.length === 0) {
|
||||
return <p className="text-muted-foreground">No articles yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{articles.map((article) => (
|
||||
<ArticleCard key={article.id} article={article} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Presentational component (receives props)
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/ui/components/article-card.tsx
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export type ArticleCardProps = { article: Article };
|
||||
|
||||
export function ArticleCard({ article }: ArticleCardProps) {
|
||||
return (
|
||||
<article className="rounded-lg border border-border bg-card p-4">
|
||||
<a href={`/blog/${article.slug}`}>
|
||||
<h3 className="text-lg font-semibold">{article.title}</h3>
|
||||
</a>
|
||||
<time className="text-sm text-muted-foreground"
|
||||
dateTime={article.createdAt.toISOString()}>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
> **No `renderLink` props.** Client components are `"use client"` —
|
||||
> functions cannot be passed from server components. Use plain `<a>` tags
|
||||
> or import the framework's Link component directly if the feature has
|
||||
> that framework as a dependency.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Export from the barrel
|
||||
|
||||
The barrel exports **server components** under clean names. Client
|
||||
components are internal — never re-exported:
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/ui/index.ts
|
||||
export { articleBySlugQuery, listArticlesQuery } from "./query";
|
||||
export { useArticleList } from "./hooks/use-article-list";
|
||||
export { useArticleBySlug } from "./hooks/use-article-by-slug";
|
||||
export { ArticleCard, type ArticleCardProps } from "./components/article-card";
|
||||
export { ArticleList } from "./components/article-list.server";
|
||||
export { ArticleDetail } from "./components/article-detail.server";
|
||||
```
|
||||
|
||||
Apps import from `@repo/<feature>/ui` — they get the server component
|
||||
which handles prefetch + hydration internally:
|
||||
|
||||
```typescript
|
||||
import { ArticleList } from "@repo/blog/ui";
|
||||
import { SiteHeader } from "@repo/navigation/ui";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## App wiring
|
||||
|
||||
### Next.js (`apps/web-next`)
|
||||
|
||||
#### Root layout — DI + providers
|
||||
|
||||
`bindAll()` runs once in the root layout. `NextTrpcProvider` wraps all
|
||||
pages with the tRPC client and React Query.
|
||||
|
||||
```typescript
|
||||
// apps/web-next/src/app/layout.tsx
|
||||
import { bindAll } from "../server/bind-production";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export default async function RootLayout({ children }) {
|
||||
await bindAll();
|
||||
return (
|
||||
<html lang="en">
|
||||
<body><Providers>{children}</Providers></body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// apps/web-next/src/app/providers.tsx
|
||||
"use client";
|
||||
import { NextTrpcProvider } from "@repo/core-trpc/next";
|
||||
|
||||
export function Providers({ children }) {
|
||||
return <NextTrpcProvider>{children}</NextTrpcProvider>;
|
||||
}
|
||||
```
|
||||
|
||||
#### Pages — just import and render
|
||||
|
||||
Feature server components handle prefetch + hydration internally. App
|
||||
pages are thin — they import the component and pass route-derived props
|
||||
(slug, id, etc.). No `appRouter`, no `queryClient`, no `HydrationBoundary`
|
||||
in the app layer:
|
||||
|
||||
```typescript
|
||||
// apps/web-next/src/app/page.tsx
|
||||
import { ArticleList } from "@repo/blog/ui";
|
||||
|
||||
export default function Home() {
|
||||
return <ArticleList />;
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// apps/web-next/src/app/blog/[slug]/page.tsx
|
||||
import { ArticleDetail } from "@repo/blog/ui";
|
||||
|
||||
export default async function BlogPostPage({ params }) {
|
||||
const { slug } = await params;
|
||||
return <ArticleDetail slug={slug} />;
|
||||
}
|
||||
```
|
||||
|
||||
The server component inside the feature resolves its controller from DI,
|
||||
prefetches data, seeds the query cache, and wraps the client component
|
||||
in `HydrationBoundary`. This gives:
|
||||
|
||||
- Full HTML on first paint (SSR)
|
||||
- Instant hydration (no loading flash)
|
||||
- Background refetch on the client via `/api/trpc`
|
||||
|
||||
> **Cache key format:** tRPC generates keys as
|
||||
> `[routerName, procedureName, { input }]`. The `setQueryData` key in the
|
||||
> server component must match what the client hook's `queryOptions`
|
||||
> generates, or the client will re-fetch instead of hydrating.
|
||||
|
||||
#### tRPC HTTP endpoint
|
||||
|
||||
Client-side queries hit `/api/trpc` after hydration:
|
||||
|
||||
```typescript
|
||||
// apps/web-next/src/app/api/trpc/[trpc]/route.ts
|
||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||
import { appRouter } from "@repo/core-api";
|
||||
|
||||
const handler = async (req: Request) =>
|
||||
fetchRequestHandler({
|
||||
endpoint: "/api/trpc",
|
||||
req,
|
||||
router: appRouter,
|
||||
createContext: () => ({}),
|
||||
});
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
```
|
||||
|
||||
### TanStack Start (`apps/web-tanstack`)
|
||||
|
||||
Same pattern but with `TanstackTrpcProvider` from `@repo/core-trpc/tanstack`
|
||||
in the root route, and TanStack Router loaders for server prefetch.
|
||||
|
||||
---
|
||||
|
||||
## Tailwind CSS in the apps
|
||||
|
||||
Both apps and Storybook need their own CSS entry point because Tailwind v4
|
||||
scans for utility classes only in files it knows about. Monorepo packages
|
||||
live outside the app directory, so `@source` directives are required.
|
||||
|
||||
```css
|
||||
/* apps/web-next/src/styles/app.css */
|
||||
@import "tailwindcss";
|
||||
@source "../../../../packages/core-ui/src";
|
||||
@source "../../../../packages/navigation/src";
|
||||
@source "../../../../packages/blog/src";
|
||||
/* ... all feature packages with UI components */
|
||||
|
||||
@import "../../../../packages/core-ui/src/styles/theme.css";
|
||||
```
|
||||
|
||||
- **Next.js** uses `@tailwindcss/postcss` via `postcss.config.mjs`
|
||||
- **Storybook** uses `@tailwindcss/vite` prepended in `viteFinal`
|
||||
- **Theme tokens** live in `packages/core-ui/src/styles/theme.css` (single
|
||||
source of truth). Both `globals.css` and app CSS files import it.
|
||||
|
||||
---
|
||||
|
||||
## Seed data & DI binding
|
||||
|
||||
### Dev seed (`USE_DEV_SEED=true` or default in development)
|
||||
|
||||
Each feature has `src/__seeds__/dev.ts` that builds realistic mock data
|
||||
using factories from `src/__factories__/`. The dev-seed binder
|
||||
(`src/di/bind-dev-seed.ts`) populates the mock repository with this data.
|
||||
|
||||
```typescript
|
||||
// packages/blog/src/__seeds__/dev.ts
|
||||
import { articleFactory } from "../__factories__/article.factory";
|
||||
|
||||
export function buildDevArticles(): Article[] {
|
||||
return [
|
||||
articleFactory.build({
|
||||
slug: "hello-world",
|
||||
title: "Hello World",
|
||||
status: "published",
|
||||
}),
|
||||
articleFactory.build({
|
||||
slug: "second-post",
|
||||
title: "Second Post",
|
||||
status: "published",
|
||||
}),
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
### Production (`USE_DEV_SEED=false` or `NODE_ENV=production`)
|
||||
|
||||
Production binders (`src/di/bind-production.ts`) replace mock repositories
|
||||
with Payload-backed implementations. They receive a `BindProductionContext`
|
||||
with `config`, `tracer`, `logger`, `queue`.
|
||||
|
||||
### Boot dispatcher
|
||||
|
||||
`apps/web-next/src/server/bind-production.ts` picks the mode:
|
||||
|
||||
| Condition | Mode |
|
||||
| --------------------- | -------------------- |
|
||||
| `USE_DEV_SEED=false` | Production (Payload) |
|
||||
| `USE_DEV_SEED=true` | Dev seed (mocks) |
|
||||
| `NODE_ENV=production` | Production |
|
||||
| Default | Dev seed |
|
||||
|
||||
Root `.env` is loaded globally via `dotenv-cli` wrapping Turbo
|
||||
(`"dev": "dotenv -- turbo run dev"` in root `package.json`).
|
||||
|
||||
### Adding a new use case to an existing feature
|
||||
|
||||
1. Add the use case to `feature.manifest.ts`
|
||||
2. Create input/output schemas in the use-case file
|
||||
3. Write the use case factory + controller
|
||||
4. Add the tRPC procedure to `integrations/api/router.ts`
|
||||
5. Wire into both `bind-production.ts` and `bind-dev-seed.ts`
|
||||
6. Add seed data to `__seeds__/dev.ts` if applicable
|
||||
7. Create a hook in `src/ui/hooks/use-<name>.ts`
|
||||
8. Create client component in `src/ui/components/<name>.client.tsx`
|
||||
9. Create server component in `src/ui/components/<name>.server.tsx`
|
||||
10. Export the server component from `src/ui/index.ts` under the clean name
|
||||
|
||||
---
|
||||
|
||||
## Cross-feature boundaries in UI
|
||||
|
||||
- Features **may** import another feature's **root barrel** (types, schemas,
|
||||
errors) but **not** its `./ui` subpath. UI composition across features
|
||||
happens in the app layer.
|
||||
- Navigation's `<SiteHeader>` receives `siteName`/`siteDescription` as
|
||||
**props** — it does not import from `@repo/marketing-pages`. The app
|
||||
page passes these scalars (the only case where the app fetches data
|
||||
that crosses feature boundaries).
|
||||
- If a page renders components from multiple features, the app page
|
||||
imports and renders them side by side — each feature component handles
|
||||
its own data fetching internally.
|
||||
|
||||
---
|
||||
|
||||
## Checklist for new feature UI
|
||||
|
||||
- [ ] Check `core-ui` for existing atoms/molecules before creating new primitives
|
||||
- [ ] Hook in `src/ui/hooks/use-<x>.ts` with `"use client"` + `useSuspenseQuery`
|
||||
- [ ] Component(s) in `src/ui/components/` composing `core-ui` primitives (atoms -> molecules -> organisms)
|
||||
- [ ] Barrel exports in `src/ui/index.ts`
|
||||
- [ ] Server component (`.server.tsx`) with DI resolve + prefetch + `HydrationBoundary`
|
||||
- [ ] Barrel exports server component under clean name (no `Server` suffix)
|
||||
- [ ] `@repo/core-trpc`, `@tanstack/react-query`, `@trpc/client`, `react` in `package.json`
|
||||
- [ ] App page just imports and renders: `<ArticleList />` or `<ArticleDetail slug={slug} />`
|
||||
- [ ] `@source` directive in app CSS for the feature package (if it has Tailwind classes)
|
||||
- [ ] Seed data in `__seeds__/dev.ts` (for dev mode)
|
||||
- [ ] Both `bind-production.ts` and `bind-dev-seed.ts` wire the new use case
|
||||
69
docs/library-decisions/2026-05-14-@trpc/react-query.md
Normal file
69
docs/library-decisions/2026-05-14-@trpc/react-query.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
package: "@trpc/react-query"
|
||||
version: "^11.0.0"
|
||||
tier: core
|
||||
decision: approved
|
||||
date: 2026-05-14
|
||||
deciders: [scaffolded]
|
||||
adr: null
|
||||
is-sub-processor: false
|
||||
processes-pii: false
|
||||
filter-results:
|
||||
license: MIT
|
||||
types: native
|
||||
maintenance: active
|
||||
boundary-fit: pass
|
||||
shadow-check: pass
|
||||
eu-residency: n/a
|
||||
cve-scan: clean
|
||||
named-consumer: pass
|
||||
socketRisk: skip
|
||||
verification-commands:
|
||||
- pnpm audit --audit-level=moderate
|
||||
- npm view @trpc/react-query license
|
||||
accepted-cves: []
|
||||
---
|
||||
|
||||
## Filter: license
|
||||
|
||||
MIT — on the workspace allowlist.
|
||||
|
||||
## Filter: types
|
||||
|
||||
Ships first-party TypeScript types; deeply integrated with tRPC's type inference.
|
||||
|
||||
## Filter: maintenance
|
||||
|
||||
Active. Maintained by the tRPC team alongside `@trpc/server` and `@trpc/client`.
|
||||
|
||||
## Filter: boundary-fit
|
||||
|
||||
Core package. The React Query integration bridge belongs in `core-trpc` alongside its sibling tRPC packages. No boundary rule violation.
|
||||
|
||||
## Filter: shadow-check
|
||||
|
||||
No other tRPC–React Query bridge in the workspace. No shadow.
|
||||
|
||||
## Filter: eu-residency
|
||||
|
||||
Client-side integration adapter; no vendor data transmission. n/a.
|
||||
|
||||
## Filter: cve-scan
|
||||
|
||||
No advisories at adoption time.
|
||||
|
||||
## Filter: named-consumer
|
||||
|
||||
`core-trpc` re-exports `@trpc/react-query` hooks for use in Next.js feature pages.
|
||||
|
||||
## Prompt: replaces
|
||||
|
||||
Nothing — this is the initial tRPC scaffold.
|
||||
|
||||
## Prompt: migration-cost-out
|
||||
|
||||
Hard: hooks are tRPC-procedure-typed; migrating away requires replacing all call sites.
|
||||
|
||||
## Prompt: alternatives-considered
|
||||
|
||||
This package is the canonical integration point between `@trpc/client` and `@tanstack/react-query`. No viable alternative exists.
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
package: "@trpc/tanstack-react-query"
|
||||
version: "^11.1.0"
|
||||
tier: core
|
||||
decision: approved
|
||||
date: 2026-05-14
|
||||
deciders: [scaffolded]
|
||||
adr: null
|
||||
is-sub-processor: false
|
||||
processes-pii: false
|
||||
filter-results:
|
||||
license: MIT
|
||||
types: native
|
||||
maintenance: active
|
||||
boundary-fit: pass
|
||||
shadow-check: pass
|
||||
eu-residency: n/a
|
||||
cve-scan: clean
|
||||
named-consumer: pass
|
||||
socketRisk: skip
|
||||
verification-commands:
|
||||
- pnpm audit --audit-level=moderate
|
||||
- npm view @trpc/tanstack-react-query license
|
||||
accepted-cves: []
|
||||
---
|
||||
|
||||
## Filter: license
|
||||
|
||||
MIT — on the workspace allowlist.
|
||||
|
||||
## Filter: types
|
||||
|
||||
Ships first-party TypeScript types; part of the tRPC v11 adapter suite.
|
||||
|
||||
## Filter: maintenance
|
||||
|
||||
Active. Maintained by the tRPC team as part of the v11 TanStack Start integration.
|
||||
|
||||
## Filter: boundary-fit
|
||||
|
||||
Core package. Required for the TanStack Start provider (`core-trpc/tanstack`). No boundary rule violation.
|
||||
|
||||
## Filter: shadow-check
|
||||
|
||||
No duplicate TanStack adapter in the workspace. No shadow.
|
||||
|
||||
## Filter: eu-residency
|
||||
|
||||
Client-side integration adapter; no vendor data transmission. n/a.
|
||||
|
||||
## Filter: cve-scan
|
||||
|
||||
No advisories at adoption time.
|
||||
|
||||
## Filter: named-consumer
|
||||
|
||||
`core-trpc` exposes a TanStack Start provider via `@trpc/tanstack-react-query` for `apps/web-tanstack`.
|
||||
|
||||
## Prompt: replaces
|
||||
|
||||
Nothing — this is the initial tRPC scaffold.
|
||||
|
||||
## Prompt: migration-cost-out
|
||||
|
||||
Hard: the TanStack provider is shaped around this adapter's API; replacing requires re-implementing the provider.
|
||||
|
||||
## Prompt: alternatives-considered
|
||||
|
||||
This is the official tRPC adapter for TanStack Start. No viable alternative exists.
|
||||
11
package.json
11
package.json
@@ -7,8 +7,8 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev",
|
||||
"build": "dotenv -- turbo run build",
|
||||
"dev": "dotenv -- turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"test": "turbo run test",
|
||||
"test:e2e": "turbo run test:e2e",
|
||||
@@ -32,19 +32,20 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ai-hero/sandcastle": "^0.5.10",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"zod": "^3.25.0",
|
||||
"@playwright/test": "^1.49.0",
|
||||
"@stryker-mutator/core": "^8.7.0",
|
||||
"@stryker-mutator/vitest-runner": "^8.7.0",
|
||||
"@turbo/gen": "^2.4.0",
|
||||
"@types/node": "^22.0.0",
|
||||
"@typescript-eslint/parser": "^8.25.0",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"fallow": "^2.73.0",
|
||||
"husky": "^9.0.0",
|
||||
"lint-staged": "^16.0.0",
|
||||
"prettier": "^3.5.0",
|
||||
"turbo": "^2.4.0",
|
||||
"typescript": "^5.8.0"
|
||||
"typescript": "^5.8.0",
|
||||
"zod": "^3.25.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js,mjs,jsx}": [
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@repo/core-trpc": "workspace:^",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"inversify": "^6.2.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
@@ -30,6 +34,7 @@
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "^3.1.0"
|
||||
}
|
||||
|
||||
23
packages/blog/src/ui/components/article-card.tsx
Normal file
23
packages/blog/src/ui/components/article-card.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export type ArticleCardProps = {
|
||||
article: Article;
|
||||
};
|
||||
|
||||
export function ArticleCard({ article }: ArticleCardProps) {
|
||||
return (
|
||||
<article className="rounded-lg border border-border bg-card p-4 transition-colors hover:bg-accent/50">
|
||||
<a href={`/blog/${article.slug}`}>
|
||||
<h3 className="text-lg font-semibold text-card-foreground">
|
||||
{article.title}
|
||||
</h3>
|
||||
</a>
|
||||
<time
|
||||
className="text-sm text-muted-foreground"
|
||||
dateTime={article.createdAt.toISOString()}
|
||||
>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
34
packages/blog/src/ui/components/article-detail.client.tsx
Normal file
34
packages/blog/src/ui/components/article-detail.client.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { useArticleBySlug } from "../hooks/use-article-by-slug";
|
||||
|
||||
export type ArticleDetailProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export function ArticleDetail({ slug }: ArticleDetailProps) {
|
||||
const { data: article } = useArticleBySlug(slug);
|
||||
|
||||
if (!article) return null;
|
||||
|
||||
return (
|
||||
<article className="mx-auto max-w-3xl">
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">{article.title}</h1>
|
||||
{article.createdAt ? (
|
||||
<time
|
||||
className="mt-2 block text-sm text-muted-foreground"
|
||||
dateTime={article.createdAt.toISOString()}
|
||||
>
|
||||
{article.createdAt.toLocaleDateString()}
|
||||
</time>
|
||||
) : null}
|
||||
</header>
|
||||
<div className="prose text-foreground">
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{JSON.stringify(article.content, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
24
packages/blog/src/ui/components/article-detail.server.tsx
Normal file
24
packages/blog/src/ui/components/article-detail.server.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { getQueryClient } from "@repo/core-trpc";
|
||||
import { blogContainer } from "../../di/container";
|
||||
import { BLOG_SYMBOLS } from "../../di/symbols";
|
||||
import type { IGetArticleBySlugController } from "../../interface-adapters/controllers/get-article-by-slug.controller";
|
||||
import { ArticleDetail as ArticleDetailClient } from "./article-detail.client";
|
||||
|
||||
export async function ArticleDetail({ slug }: { slug: string }) {
|
||||
const controller = blogContainer.get<IGetArticleBySlugController>(
|
||||
BLOG_SYMBOLS.IGetArticleBySlugController,
|
||||
);
|
||||
const article = await controller({ slug });
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(
|
||||
["blog", "articleBySlug", { input: { slug } }],
|
||||
article,
|
||||
);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticleDetailClient slug={slug} />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
20
packages/blog/src/ui/components/article-list.client.tsx
Normal file
20
packages/blog/src/ui/components/article-list.client.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { useArticleList } from "../hooks/use-article-list";
|
||||
import { ArticleCard } from "./article-card";
|
||||
|
||||
export function ArticleList() {
|
||||
const { data: articles } = useArticleList();
|
||||
|
||||
if (articles.length === 0) {
|
||||
return <p className="text-muted-foreground">No published articles yet.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-4">
|
||||
{articles.map((article) => (
|
||||
<ArticleCard key={article.id} article={article} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
packages/blog/src/ui/components/article-list.server.tsx
Normal file
24
packages/blog/src/ui/components/article-list.server.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { getQueryClient } from "@repo/core-trpc";
|
||||
import { blogContainer } from "../../di/container";
|
||||
import { BLOG_SYMBOLS } from "../../di/symbols";
|
||||
import type { IGetArticlesController } from "../../interface-adapters/controllers/get-articles.controller";
|
||||
import { ArticleList as ArticleListClient } from "./article-list.client";
|
||||
|
||||
export async function ArticleList() {
|
||||
const controller = blogContainer.get<IGetArticlesController>(
|
||||
BLOG_SYMBOLS.IGetArticlesController,
|
||||
);
|
||||
const articles = await controller({ status: "published", limit: 20 });
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(
|
||||
["blog", "listArticles", { input: { status: "published", limit: 20 } }],
|
||||
articles,
|
||||
);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<ArticleListClient />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
12
packages/blog/src/ui/hooks/use-article-by-slug.ts
Normal file
12
packages/blog/src/ui/hooks/use-article-by-slug.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export function useArticleBySlug(slug: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.blog.articleBySlug.queryOptions({ slug })) as {
|
||||
data: Article | null;
|
||||
};
|
||||
}
|
||||
18
packages/blog/src/ui/hooks/use-article-list.ts
Normal file
18
packages/blog/src/ui/hooks/use-article-list.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Article } from "../../entities/models/article";
|
||||
|
||||
export function useArticleList(options?: {
|
||||
status?: "draft" | "published";
|
||||
limit?: number;
|
||||
}) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(
|
||||
trpc.blog.listArticles.queryOptions({
|
||||
status: options?.status ?? "published",
|
||||
limit: options?.limit ?? 20,
|
||||
}),
|
||||
) as { data: Article[] };
|
||||
}
|
||||
@@ -1 +1,6 @@
|
||||
export { articleBySlugQuery, listArticlesQuery } from "./query";
|
||||
export { useArticleList } from "./hooks/use-article-list";
|
||||
export { useArticleBySlug } from "./hooks/use-article-by-slug";
|
||||
export { ArticleCard, type ArticleCardProps } from "./components/article-card";
|
||||
export { ArticleList } from "./components/article-list.server";
|
||||
export { ArticleDetail } from "./components/article-detail.server";
|
||||
|
||||
3
packages/core-trpc/eslint.config.js
Normal file
3
packages/core-trpc/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/core-eslint/base";
|
||||
|
||||
export default baseConfig;
|
||||
37
packages/core-trpc/package.json
Normal file
37
packages/core-trpc/package.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@repo/core-trpc",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./next": "./src/providers/next-provider.tsx",
|
||||
"./tanstack": "./src/providers/tanstack-provider.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-api": "workspace:*",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/react-query": "^11.17.0",
|
||||
"@trpc/server": "^11.17.0",
|
||||
"@trpc/tanstack-react-query": "^11.17.0",
|
||||
"react": "^19.0.0",
|
||||
"superjson": "^2.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
12
packages/core-trpc/src/client.test.ts
Normal file
12
packages/core-trpc/src/client.test.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { useTRPC, TRPCProvider } from "./client";
|
||||
|
||||
describe("core-trpc client exports", () => {
|
||||
it("exports useTRPC hook", () => {
|
||||
expect(useTRPC).toBeTypeOf("function");
|
||||
});
|
||||
|
||||
it("exports TRPCProvider component", () => {
|
||||
expect(TRPCProvider).toBeTypeOf("function");
|
||||
});
|
||||
});
|
||||
6
packages/core-trpc/src/client.ts
Normal file
6
packages/core-trpc/src/client.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { createTRPCContext } from "@trpc/tanstack-react-query";
|
||||
import type { AppRouter } from "@repo/core-api";
|
||||
|
||||
export const { TRPCProvider, useTRPC } = createTRPCContext<AppRouter>();
|
||||
3
packages/core-trpc/src/index.ts
Normal file
3
packages/core-trpc/src/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { useTRPC, TRPCProvider } from "./client";
|
||||
export { getQueryClient } from "./query-client";
|
||||
export type { AppRouter } from "@repo/core-api";
|
||||
40
packages/core-trpc/src/providers/next-provider.tsx
Normal file
40
packages/core-trpc/src/providers/next-provider.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
import type { AppRouter } from "@repo/core-api";
|
||||
import { TRPCProvider } from "../client";
|
||||
import { getQueryClient } from "../query-client";
|
||||
|
||||
function getBaseUrl() {
|
||||
if (typeof window !== "undefined") return "";
|
||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
||||
}
|
||||
|
||||
export function NextTrpcProvider({
|
||||
children,
|
||||
trpcUrl = "/api/trpc",
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
trpcUrl?: string;
|
||||
}) {
|
||||
const [queryClient] = useState(() => getQueryClient());
|
||||
const [trpcClient] = useState(() =>
|
||||
createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${getBaseUrl()}${trpcUrl}`,
|
||||
transformer: superjson,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</TRPCProvider>
|
||||
);
|
||||
}
|
||||
30
packages/core-trpc/src/providers/tanstack-provider.tsx
Normal file
30
packages/core-trpc/src/providers/tanstack-provider.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { createTRPCClient, httpBatchLink } from "@trpc/client";
|
||||
import superjson from "superjson";
|
||||
import type { AppRouter } from "@repo/core-api";
|
||||
import { TRPCProvider } from "../client";
|
||||
import { getQueryClient } from "../query-client";
|
||||
|
||||
export function TanstackTrpcProvider({
|
||||
children,
|
||||
trpcUrl,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
trpcUrl: string;
|
||||
}) {
|
||||
const [queryClient] = useState(() => getQueryClient());
|
||||
const [trpcClient] = useState(() =>
|
||||
createTRPCClient<AppRouter>({
|
||||
links: [httpBatchLink({ url: trpcUrl, transformer: superjson })],
|
||||
}),
|
||||
);
|
||||
|
||||
return (
|
||||
<TRPCProvider trpcClient={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
</TRPCProvider>
|
||||
);
|
||||
}
|
||||
22
packages/core-trpc/src/query-client.ts
Normal file
22
packages/core-trpc/src/query-client.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { QueryClient } from "@tanstack/react-query";
|
||||
|
||||
let clientQueryClient: QueryClient | undefined;
|
||||
|
||||
const defaultOptions = {
|
||||
queries: {
|
||||
staleTime: 30 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
};
|
||||
|
||||
export function getQueryClient(): QueryClient {
|
||||
if (typeof window === "undefined") {
|
||||
// Server: always create a new instance per request
|
||||
return new QueryClient({ defaultOptions });
|
||||
}
|
||||
// Browser: singleton
|
||||
if (!clientQueryClient) {
|
||||
clientQueryClient = new QueryClient({ defaultOptions });
|
||||
}
|
||||
return clientQueryClient;
|
||||
}
|
||||
17
packages/core-trpc/tsconfig.json
Normal file
17
packages/core-trpc/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "@repo/core-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"jsx": "preserve",
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"types": ["vitest/globals", "@testing-library/jest-dom"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4
packages/core-trpc/turbo.json
Normal file
4
packages/core-trpc/turbo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tags": ["core-composition"]
|
||||
}
|
||||
7
packages/core-trpc/vitest.config.ts
Normal file
7
packages/core-trpc/vitest.config.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import path from "node:path";
|
||||
import { mergeConfig } from "vitest/config";
|
||||
import { jsdomVitestConfig } from "@repo/core-typescript/vitest.base.jsdom";
|
||||
|
||||
export default mergeConfig(jsdomVitestConfig, {
|
||||
resolve: { alias: { "@": path.resolve(__dirname, "./src") } },
|
||||
});
|
||||
@@ -24,9 +24,13 @@
|
||||
"dependencies": {
|
||||
"@repo/auth": "workspace:*",
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@repo/core-trpc": "workspace:^",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"inversify": "^6.2.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
@@ -35,6 +39,7 @@
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "^3.1.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"use client";
|
||||
|
||||
import { usePageBySlug } from "../hooks/use-page-by-slug";
|
||||
import { PageHero } from "./page-hero";
|
||||
|
||||
export type PageContentProps = {
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export function PageContent({ slug }: PageContentProps) {
|
||||
const { data: page } = usePageBySlug(slug);
|
||||
|
||||
if (!page) return null;
|
||||
|
||||
return (
|
||||
<article className="mx-auto max-w-3xl">
|
||||
<PageHero hero={page.hero} />
|
||||
<div className="prose text-foreground">
|
||||
<pre className="whitespace-pre-wrap text-sm">
|
||||
{JSON.stringify(page.layout, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { getQueryClient } from "@repo/core-trpc";
|
||||
import { marketingPagesContainer } from "../../di/container";
|
||||
import { MARKETING_PAGES_SYMBOLS } from "../../di/symbols";
|
||||
import type { IGetPageBySlugController } from "../../interface-adapters/controllers/get-page-by-slug.controller";
|
||||
import { PageContent as PageContentClient } from "./page-content.client";
|
||||
|
||||
export async function PageContent({ slug }: { slug: string }) {
|
||||
const controller = marketingPagesContainer.get<IGetPageBySlugController>(
|
||||
MARKETING_PAGES_SYMBOLS.IGetPageBySlugController,
|
||||
);
|
||||
const page = await controller({ slug });
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(
|
||||
["marketingPages", "pageBySlug", { input: { slug } }],
|
||||
page,
|
||||
);
|
||||
|
||||
if (!page) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-6 py-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">Not found</h1>
|
||||
<p className="mt-2 text-muted-foreground">
|
||||
This page hasn't been published yet.
|
||||
</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<PageContentClient slug={slug} />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
16
packages/marketing-pages/src/ui/components/page-hero.tsx
Normal file
16
packages/marketing-pages/src/ui/components/page-hero.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { Hero } from "../../entities/models/page";
|
||||
|
||||
export type PageHeroProps = {
|
||||
hero: Hero;
|
||||
};
|
||||
|
||||
export function PageHero({ hero }: PageHeroProps) {
|
||||
return (
|
||||
<header className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-foreground">{hero.heading}</h1>
|
||||
{hero.subheading ? (
|
||||
<p className="mt-2 text-lg text-muted-foreground">{hero.subheading}</p>
|
||||
) : null}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
12
packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts
Normal file
12
packages/marketing-pages/src/ui/hooks/use-page-by-slug.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { Page } from "../../entities/models/page";
|
||||
|
||||
export function usePageBySlug(slug: string) {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(
|
||||
trpc.marketingPages.pageBySlug.queryOptions({ slug }),
|
||||
) as { data: Page | null };
|
||||
}
|
||||
12
packages/marketing-pages/src/ui/hooks/use-site-settings.ts
Normal file
12
packages/marketing-pages/src/ui/hooks/use-site-settings.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
import type { SiteSettings } from "../../entities/models/site-settings";
|
||||
|
||||
export function useSiteSettings() {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(
|
||||
trpc.marketingPages.siteSettings.queryOptions({}),
|
||||
) as { data: SiteSettings };
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
export { pageBySlugQuery, siteSettingsQuery } from "./query";
|
||||
export { usePageBySlug } from "./hooks/use-page-by-slug";
|
||||
export { useSiteSettings } from "./hooks/use-site-settings";
|
||||
export { PageHero, type PageHeroProps } from "./components/page-hero";
|
||||
export { PageContent } from "./components/page-content.server";
|
||||
|
||||
@@ -19,9 +19,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-shared": "workspace:*",
|
||||
"@repo/core-trpc": "workspace:^",
|
||||
"@tanstack/react-query": "^5.66.0",
|
||||
"@trpc/client": "^11.17.0",
|
||||
"@trpc/server": "^11.0.0",
|
||||
"inversify": "^6.2.0",
|
||||
"payload": "^3.14.0",
|
||||
"react": "^19.0.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
@@ -30,6 +34,7 @@
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@types/node": "^22.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "^3.1.0"
|
||||
}
|
||||
|
||||
46
packages/navigation/src/ui/components/site-header.client.tsx
Normal file
46
packages/navigation/src/ui/components/site-header.client.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import { useHeader } from "../hooks/use-header";
|
||||
|
||||
export type SiteHeaderProps = {
|
||||
siteName: string;
|
||||
siteDescription?: string;
|
||||
};
|
||||
|
||||
export function SiteHeader({ siteName, siteDescription }: SiteHeaderProps) {
|
||||
const { data: header } = useHeader();
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background px-6 py-4">
|
||||
<div className="mx-auto flex max-w-5xl items-center justify-between">
|
||||
<div>
|
||||
<a href="/">
|
||||
<span className="text-lg font-semibold text-foreground">
|
||||
{siteName}
|
||||
</span>
|
||||
</a>
|
||||
{siteDescription ? (
|
||||
<p className="text-sm text-muted-foreground">{siteDescription}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<nav>
|
||||
<ul className="flex gap-4">
|
||||
{header.items.map((item) => (
|
||||
<li key={item.href}>
|
||||
<a
|
||||
href={item.href}
|
||||
{...(item.external
|
||||
? { target: "_blank", rel: "noopener noreferrer" }
|
||||
: {})}
|
||||
className="text-sm font-medium text-foreground hover:text-primary"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
27
packages/navigation/src/ui/components/site-header.server.tsx
Normal file
27
packages/navigation/src/ui/components/site-header.server.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { dehydrate, HydrationBoundary } from "@tanstack/react-query";
|
||||
import { getQueryClient } from "@repo/core-trpc";
|
||||
import { navigationContainer } from "../../di/container";
|
||||
import { NAVIGATION_SYMBOLS } from "../../di/symbols";
|
||||
import type { IGetHeaderController } from "../../interface-adapters/controllers/get-header.controller";
|
||||
import { SiteHeader as SiteHeaderClient } from "./site-header.client";
|
||||
|
||||
export async function SiteHeader({
|
||||
siteName,
|
||||
siteDescription,
|
||||
}: {
|
||||
siteName: string;
|
||||
siteDescription?: string;
|
||||
}) {
|
||||
const controller = navigationContainer.get<IGetHeaderController>(
|
||||
NAVIGATION_SYMBOLS.IGetHeaderController,
|
||||
);
|
||||
const header = await controller({});
|
||||
const queryClient = getQueryClient();
|
||||
queryClient.setQueryData(["navigation", "header", { input: {} }], header);
|
||||
|
||||
return (
|
||||
<HydrationBoundary state={dehydrate(queryClient)}>
|
||||
<SiteHeaderClient siteName={siteName} siteDescription={siteDescription} />
|
||||
</HydrationBoundary>
|
||||
);
|
||||
}
|
||||
15
packages/navigation/src/ui/hooks/use-header.ts
Normal file
15
packages/navigation/src/ui/hooks/use-header.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { useSuspenseQuery } from "@tanstack/react-query";
|
||||
import { useTRPC } from "@repo/core-trpc";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
||||
export function useHeader() {
|
||||
const trpc = useTRPC();
|
||||
return useSuspenseQuery(trpc.navigation.header.queryOptions({})) as {
|
||||
data: {
|
||||
items: { label: string; href: string; external: boolean }[];
|
||||
logoId?: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
export { headerQuery } from "./query";
|
||||
export { useHeader } from "./hooks/use-header";
|
||||
export { SiteHeader } from "./components/site-header.server";
|
||||
|
||||
489
pnpm-lock.yaml
generated
489
pnpm-lock.yaml
generated
@@ -8,7 +8,7 @@ importers:
|
||||
.:
|
||||
devDependencies:
|
||||
"@ai-hero/sandcastle":
|
||||
specifier: "^0.5.10"
|
||||
specifier: ^0.5.10
|
||||
version: 0.5.10(@effect/cluster@0.57.0(@effect/platform@0.95.0(effect@3.21.2))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2)(ioredis@5.10.1))(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2))(@effect/workflow@0.17.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2)(ioredis@5.10.1))(@effect/platform@0.95.0(effect@3.21.2))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2))(effect@3.21.2))(effect@3.21.2))(@effect/rpc@0.74.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2))(@effect/sql@0.50.0(@effect/experimental@0.59.0(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2)(ioredis@5.10.1))(@effect/platform@0.95.0(effect@3.21.2))(effect@3.21.2))(@effect/typeclass@0.39.0(effect@3.21.2))
|
||||
"@playwright/test":
|
||||
specifier: ^1.49.0
|
||||
@@ -28,6 +28,9 @@ importers:
|
||||
"@typescript-eslint/parser":
|
||||
specifier: ^8.25.0
|
||||
version: 8.58.0(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)
|
||||
dotenv-cli:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
fallow:
|
||||
specifier: ^2.73.0
|
||||
version: 2.73.0
|
||||
@@ -182,6 +185,9 @@ importers:
|
||||
"@repo/core-shared":
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/core-shared
|
||||
"@repo/core-trpc":
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/core-trpc
|
||||
"@repo/marketing-pages":
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/marketing-pages
|
||||
@@ -194,12 +200,15 @@ importers:
|
||||
"@sentry/nextjs":
|
||||
specifier: ^10.51.0
|
||||
version: 10.51.0(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(next@15.5.14(@babel/core@7.25.9)(@opentelemetry/api@1.9.1)(@playwright/test@1.59.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.99.0))(react@19.2.4)(webpack@5.106.2)
|
||||
"@tailwindcss/postcss":
|
||||
specifier: ^4.3.0
|
||||
version: 4.3.0
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.66.0
|
||||
specifier: ^5.96.2
|
||||
version: 5.96.2(react@19.2.4)
|
||||
"@trpc/server":
|
||||
specifier: ^11.0.0
|
||||
version: 11.16.0(typescript@5.9.3)
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(typescript@5.9.3)
|
||||
inversify:
|
||||
specifier: ^6.2.0
|
||||
version: 6.2.2(reflect-metadata@0.2.2)
|
||||
@@ -221,6 +230,9 @@ importers:
|
||||
superjson:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.6
|
||||
tailwindcss:
|
||||
specifier: ^4.1.0
|
||||
version: 4.2.2
|
||||
devDependencies:
|
||||
"@playwright/test":
|
||||
specifier: ^1.50.0
|
||||
@@ -389,6 +401,15 @@ importers:
|
||||
"@repo/core-shared":
|
||||
specifier: workspace:*
|
||||
version: link:../core-shared
|
||||
"@repo/core-trpc":
|
||||
specifier: workspace:^
|
||||
version: link:../core-trpc
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.66.0
|
||||
version: 5.96.2(react@19.2.4)
|
||||
"@trpc/client":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/server":
|
||||
specifier: ^11.0.0
|
||||
version: 11.16.0(typescript@5.9.3)
|
||||
@@ -398,6 +419,9 @@ importers:
|
||||
payload:
|
||||
specifier: ^3.14.0
|
||||
version: 3.81.0(graphql@16.13.2)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@@ -417,6 +441,9 @@ importers:
|
||||
"@types/node":
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.17
|
||||
"@types/react":
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
"@vitest/coverage-v8":
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
|
||||
@@ -871,6 +898,58 @@ importers:
|
||||
specifier: ^5.8.0
|
||||
version: 5.9.3
|
||||
|
||||
packages/core-trpc:
|
||||
dependencies:
|
||||
"@repo/core-api":
|
||||
specifier: workspace:*
|
||||
version: link:../core-api
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.66.0
|
||||
version: 5.96.2(react@19.2.4)
|
||||
"@trpc/client":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/react-query":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@tanstack/react-query@5.96.2(react@19.2.4))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.4)(typescript@5.9.3)
|
||||
"@trpc/server":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(typescript@5.9.3)
|
||||
"@trpc/tanstack-react-query":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@tanstack/react-query@5.96.2(react@19.2.4))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.4)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
superjson:
|
||||
specifier: ^2.2.1
|
||||
version: 2.2.6
|
||||
devDependencies:
|
||||
"@repo/core-eslint":
|
||||
specifier: workspace:*
|
||||
version: link:../core-eslint
|
||||
"@repo/core-testing":
|
||||
specifier: workspace:*
|
||||
version: link:../core-testing
|
||||
"@repo/core-typescript":
|
||||
specifier: workspace:*
|
||||
version: link:../core-typescript
|
||||
"@testing-library/jest-dom":
|
||||
specifier: ^6.5.0
|
||||
version: 6.9.1
|
||||
"@testing-library/react":
|
||||
specifier: ^16.0.0
|
||||
version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
"@types/react":
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
jsdom:
|
||||
specifier: ^25.0.0
|
||||
version: 25.0.1
|
||||
vitest:
|
||||
specifier: ^3.0.0
|
||||
version: 3.2.4(@types/debug@4.1.13)(@types/node@25.5.2)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0)
|
||||
|
||||
packages/core-typescript:
|
||||
devDependencies:
|
||||
"@vitest/coverage-v8":
|
||||
@@ -943,6 +1022,15 @@ importers:
|
||||
"@repo/core-shared":
|
||||
specifier: workspace:*
|
||||
version: link:../core-shared
|
||||
"@repo/core-trpc":
|
||||
specifier: workspace:^
|
||||
version: link:../core-trpc
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.66.0
|
||||
version: 5.96.2(react@19.2.4)
|
||||
"@trpc/client":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/server":
|
||||
specifier: ^11.0.0
|
||||
version: 11.16.0(typescript@5.9.3)
|
||||
@@ -952,6 +1040,9 @@ importers:
|
||||
payload:
|
||||
specifier: ^3.14.0
|
||||
version: 3.81.0(graphql@16.13.2)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@@ -971,6 +1062,9 @@ importers:
|
||||
"@types/node":
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.17
|
||||
"@types/react":
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
"@vitest/coverage-v8":
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
|
||||
@@ -1023,6 +1117,15 @@ importers:
|
||||
"@repo/core-shared":
|
||||
specifier: workspace:*
|
||||
version: link:../core-shared
|
||||
"@repo/core-trpc":
|
||||
specifier: workspace:^
|
||||
version: link:../core-trpc
|
||||
"@tanstack/react-query":
|
||||
specifier: ^5.66.0
|
||||
version: 5.96.2(react@19.2.4)
|
||||
"@trpc/client":
|
||||
specifier: ^11.17.0
|
||||
version: 11.17.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/server":
|
||||
specifier: ^11.0.0
|
||||
version: 11.16.0(typescript@5.9.3)
|
||||
@@ -1032,6 +1135,9 @@ importers:
|
||||
payload:
|
||||
specifier: ^3.14.0
|
||||
version: 3.81.0(graphql@16.13.2)(typescript@5.9.3)
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
reflect-metadata:
|
||||
specifier: ^0.2.2
|
||||
version: 0.2.2
|
||||
@@ -1051,6 +1157,9 @@ importers:
|
||||
"@types/node":
|
||||
specifier: ^22.0.0
|
||||
version: 22.19.17
|
||||
"@types/react":
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.14
|
||||
"@vitest/coverage-v8":
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.17)(happy-dom@20.8.9)(jiti@2.7.0)(jsdom@25.0.1)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))
|
||||
@@ -1099,6 +1208,13 @@ packages:
|
||||
"@vercel/sandbox":
|
||||
optional: true
|
||||
|
||||
"@alloc/quick-lru@5.2.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==,
|
||||
}
|
||||
engines: { node: ">=10" }
|
||||
|
||||
"@ampproject/remapping@2.3.0":
|
||||
resolution:
|
||||
{
|
||||
@@ -6994,6 +7110,12 @@ packages:
|
||||
integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==,
|
||||
}
|
||||
|
||||
"@tailwindcss/node@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==,
|
||||
}
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7003,6 +7125,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7012,6 +7143,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7021,6 +7161,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7030,6 +7179,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7039,6 +7197,15 @@ packages:
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7048,6 +7215,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7057,6 +7233,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7066,6 +7251,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7075,6 +7269,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7090,6 +7293,21 @@ packages:
|
||||
- "@emnapi/wasi-threads"
|
||||
- tslib
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==,
|
||||
}
|
||||
engines: { node: ">=14.0.0" }
|
||||
cpu: [wasm32]
|
||||
bundledDependencies:
|
||||
- "@napi-rs/wasm-runtime"
|
||||
- "@emnapi/core"
|
||||
- "@emnapi/runtime"
|
||||
- "@tybys/wasm-util"
|
||||
- "@emnapi/wasi-threads"
|
||||
- tslib
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7099,6 +7317,15 @@ packages:
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7108,6 +7335,15 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
"@tailwindcss/oxide@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7115,6 +7351,19 @@ packages:
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
|
||||
"@tailwindcss/oxide@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==,
|
||||
}
|
||||
engines: { node: ">= 20" }
|
||||
|
||||
"@tailwindcss/postcss@4.3.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-Jm05Tjx+9yCLGv5qw1c+84Psds8MnyrEQYCB+FFk2lgGiUjlRqdxke4mVTuYrj2xnVZqKim2Apr5ySuQRYAw/w==,
|
||||
}
|
||||
|
||||
"@tailwindcss/vite@4.2.2":
|
||||
resolution:
|
||||
{
|
||||
@@ -7569,6 +7818,28 @@ packages:
|
||||
"@trpc/server": 11.16.0
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@trpc/client@11.17.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-KpJBFrbKTDeVCFv/3ckL1XBBH5Yssn8hethI/rUy7GIpTj+VzjtPjykDqJpzobuVOz+d26cXCSu1t4I6MYI5Zg==,
|
||||
}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
"@trpc/server": 11.17.0
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@trpc/react-query@11.17.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-AGcl5YAF8NnhBmyJ6PqJqKb1M5VTGSoNRNqJ3orct4o4epdcg0GWhW+qT9q6gPzs/2ImIwYCdfFpgNGdZ9yLHA==,
|
||||
}
|
||||
peerDependencies:
|
||||
"@tanstack/react-query": ^5.80.3
|
||||
"@trpc/client": 11.17.0
|
||||
"@trpc/server": 11.17.0
|
||||
react: ">=18.2.0"
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@trpc/server@11.16.0":
|
||||
resolution:
|
||||
{
|
||||
@@ -7578,6 +7849,28 @@ packages:
|
||||
peerDependencies:
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@trpc/server@11.17.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-jbAOUe0PpUTCYqziyu+8vYXZdDXPudZgnEhWCQ2NjKnVEjfE93RqHTt1oycZJv/HNf51YlRXfEEwSIAbb161rw==,
|
||||
}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@trpc/tanstack-react-query@11.17.0":
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-OFxjvCgisP0yaCj7lgP6qPaFwJvJDgnrAUxH3cNIPPR89TpYbndn5vsdKNkc7EnvER9b6wq9Yz8aCcPcOBpDXg==,
|
||||
}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
"@tanstack/react-query": ^5.80.3
|
||||
"@trpc/client": 11.17.0
|
||||
"@trpc/server": 11.17.0
|
||||
react: ">=18.2.0"
|
||||
typescript: ">=5.7.2"
|
||||
|
||||
"@turbo/darwin-64@2.9.4":
|
||||
resolution:
|
||||
{
|
||||
@@ -9914,6 +10207,20 @@ packages:
|
||||
}
|
||||
engines: { node: ">=20" }
|
||||
|
||||
dotenv-cli@11.0.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==,
|
||||
}
|
||||
hasBin: true
|
||||
|
||||
dotenv-expand@12.0.3:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==,
|
||||
}
|
||||
engines: { node: ">=12" }
|
||||
|
||||
dotenv@16.0.3:
|
||||
resolution:
|
||||
{
|
||||
@@ -10131,6 +10438,13 @@ packages:
|
||||
}
|
||||
engines: { node: ">=10.13.0" }
|
||||
|
||||
enhanced-resolve@5.22.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-xYcDWrpELkFzz9SpZ3PlI6Eu6eD93Yf0WLDRxikGhWJ3MAir2SNZTIVCVZqZ/NUyx8AdMc2gT9C0gPiw18kG+A==,
|
||||
}
|
||||
engines: { node: ">=10.13.0" }
|
||||
|
||||
entities@1.1.2:
|
||||
resolution:
|
||||
{
|
||||
@@ -13301,6 +13615,14 @@ packages:
|
||||
engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
|
||||
hasBin: true
|
||||
|
||||
nanoid@3.3.12:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==,
|
||||
}
|
||||
engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 }
|
||||
hasBin: true
|
||||
|
||||
natural-compare@1.4.0:
|
||||
resolution:
|
||||
{
|
||||
@@ -14032,6 +14354,13 @@ packages:
|
||||
}
|
||||
engines: { node: ^10 || ^12 || >=14 }
|
||||
|
||||
postcss@8.5.15:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==,
|
||||
}
|
||||
engines: { node: ^10 || ^12 || >=14 }
|
||||
|
||||
postcss@8.5.8:
|
||||
resolution:
|
||||
{
|
||||
@@ -15472,6 +15801,12 @@ packages:
|
||||
integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==,
|
||||
}
|
||||
|
||||
tailwindcss@4.3.0:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==,
|
||||
}
|
||||
|
||||
tapable@2.3.2:
|
||||
resolution:
|
||||
{
|
||||
@@ -15479,6 +15814,13 @@ packages:
|
||||
}
|
||||
engines: { node: ">=6" }
|
||||
|
||||
tapable@2.3.3:
|
||||
resolution:
|
||||
{
|
||||
integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==,
|
||||
}
|
||||
engines: { node: ">=6" }
|
||||
|
||||
tar-stream@3.2.0:
|
||||
resolution:
|
||||
{
|
||||
@@ -16822,6 +17164,8 @@ snapshots:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
"@alloc/quick-lru@5.2.0": {}
|
||||
|
||||
"@ampproject/remapping@2.3.0":
|
||||
dependencies:
|
||||
"@jridgewell/gen-mapping": 0.3.13
|
||||
@@ -21111,42 +21455,88 @@ snapshots:
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.2.2
|
||||
|
||||
"@tailwindcss/node@4.3.0":
|
||||
dependencies:
|
||||
"@jridgewell/remapping": 2.3.5
|
||||
enhanced-resolve: 5.22.0
|
||||
jiti: 2.7.0
|
||||
lightningcss: 1.32.0
|
||||
magic-string: 0.30.21
|
||||
source-map-js: 1.2.1
|
||||
tailwindcss: 4.3.0
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-android-arm64@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-darwin-x64@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-freebsd-x64@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-gnu@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-arm64-musl@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-gnu@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-linux-x64-musl@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-wasm32-wasi@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-win32-arm64-msvc@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.2.2":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide-win32-x64-msvc@4.3.0":
|
||||
optional: true
|
||||
|
||||
"@tailwindcss/oxide@4.2.2":
|
||||
optionalDependencies:
|
||||
"@tailwindcss/oxide-android-arm64": 4.2.2
|
||||
@@ -21162,6 +21552,29 @@ snapshots:
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": 4.2.2
|
||||
"@tailwindcss/oxide-win32-x64-msvc": 4.2.2
|
||||
|
||||
"@tailwindcss/oxide@4.3.0":
|
||||
optionalDependencies:
|
||||
"@tailwindcss/oxide-android-arm64": 4.3.0
|
||||
"@tailwindcss/oxide-darwin-arm64": 4.3.0
|
||||
"@tailwindcss/oxide-darwin-x64": 4.3.0
|
||||
"@tailwindcss/oxide-freebsd-x64": 4.3.0
|
||||
"@tailwindcss/oxide-linux-arm-gnueabihf": 4.3.0
|
||||
"@tailwindcss/oxide-linux-arm64-gnu": 4.3.0
|
||||
"@tailwindcss/oxide-linux-arm64-musl": 4.3.0
|
||||
"@tailwindcss/oxide-linux-x64-gnu": 4.3.0
|
||||
"@tailwindcss/oxide-linux-x64-musl": 4.3.0
|
||||
"@tailwindcss/oxide-wasm32-wasi": 4.3.0
|
||||
"@tailwindcss/oxide-win32-arm64-msvc": 4.3.0
|
||||
"@tailwindcss/oxide-win32-x64-msvc": 4.3.0
|
||||
|
||||
"@tailwindcss/postcss@4.3.0":
|
||||
dependencies:
|
||||
"@alloc/quick-lru": 5.2.0
|
||||
"@tailwindcss/node": 4.3.0
|
||||
"@tailwindcss/oxide": 4.3.0
|
||||
postcss: 8.5.15
|
||||
tailwindcss: 4.3.0
|
||||
|
||||
"@tailwindcss/vite@4.2.2(vite@6.4.2(@types/node@22.19.17)(jiti@2.7.0)(lightningcss@1.32.0)(sass@1.99.0)(terser@5.46.2)(tsx@4.21.0)(yaml@2.9.0))":
|
||||
dependencies:
|
||||
"@tailwindcss/node": 4.2.2
|
||||
@@ -21908,10 +22321,40 @@ snapshots:
|
||||
"@trpc/server": 11.16.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/client@11.17.0(@trpc/server@11.16.0(typescript@5.9.3))(typescript@5.9.3)":
|
||||
dependencies:
|
||||
"@trpc/server": 11.16.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3)":
|
||||
dependencies:
|
||||
"@trpc/server": 11.17.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/react-query@11.17.0(@tanstack/react-query@5.96.2(react@19.2.4))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.4)(typescript@5.9.3)":
|
||||
dependencies:
|
||||
"@tanstack/react-query": 5.96.2(react@19.2.4)
|
||||
"@trpc/client": 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/server": 11.17.0(typescript@5.9.3)
|
||||
react: 19.2.4
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/server@11.16.0(typescript@5.9.3)":
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/server@11.17.0(typescript@5.9.3)":
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
||||
"@trpc/tanstack-react-query@11.17.0(@tanstack/react-query@5.96.2(react@19.2.4))(@trpc/client@11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.17.0(typescript@5.9.3))(react@19.2.4)(typescript@5.9.3)":
|
||||
dependencies:
|
||||
"@tanstack/react-query": 5.96.2(react@19.2.4)
|
||||
"@trpc/client": 11.17.0(@trpc/server@11.17.0(typescript@5.9.3))(typescript@5.9.3)
|
||||
"@trpc/server": 11.17.0(typescript@5.9.3)
|
||||
react: 19.2.4
|
||||
typescript: 5.9.3
|
||||
|
||||
"@turbo/darwin-64@2.9.4":
|
||||
optional: true
|
||||
|
||||
@@ -22845,7 +23288,7 @@ snapshots:
|
||||
dotenv: 17.4.2
|
||||
exsolve: 1.0.8
|
||||
giget: 3.2.0
|
||||
jiti: 2.6.1
|
||||
jiti: 2.7.0
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
perfect-debounce: 2.1.0
|
||||
@@ -23371,6 +23814,17 @@ snapshots:
|
||||
dependencies:
|
||||
type-fest: 5.6.0
|
||||
|
||||
dotenv-cli@11.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
dotenv: 17.4.2
|
||||
dotenv-expand: 12.0.3
|
||||
minimist: 1.2.8
|
||||
|
||||
dotenv-expand@12.0.3:
|
||||
dependencies:
|
||||
dotenv: 16.6.1
|
||||
|
||||
dotenv@16.0.3: {}
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
@@ -23444,6 +23898,11 @@ snapshots:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.2
|
||||
|
||||
enhanced-resolve@5.22.0:
|
||||
dependencies:
|
||||
graceful-fs: 4.2.11
|
||||
tapable: 2.3.3
|
||||
|
||||
entities@1.1.2: {}
|
||||
|
||||
entities@2.2.0: {}
|
||||
@@ -25287,7 +25746,7 @@ snapshots:
|
||||
get-port-please: 3.2.0
|
||||
h3: 1.15.11
|
||||
http-shutdown: 1.2.2
|
||||
jiti: 2.6.1
|
||||
jiti: 2.7.0
|
||||
mlly: 1.8.2
|
||||
node-forge: 1.4.0
|
||||
pathe: 2.0.3
|
||||
@@ -25758,6 +26217,8 @@ snapshots:
|
||||
|
||||
nanoid@3.3.11: {}
|
||||
|
||||
nanoid@3.3.12: {}
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
neo-async@2.6.2: {}
|
||||
@@ -26352,6 +26813,12 @@ snapshots:
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.15:
|
||||
dependencies:
|
||||
nanoid: 3.3.12
|
||||
picocolors: 1.1.1
|
||||
source-map-js: 1.2.1
|
||||
|
||||
postcss@8.5.8:
|
||||
dependencies:
|
||||
nanoid: 3.3.11
|
||||
@@ -27262,8 +27729,12 @@ snapshots:
|
||||
|
||||
tailwindcss@4.2.2: {}
|
||||
|
||||
tailwindcss@4.3.0: {}
|
||||
|
||||
tapable@2.3.2: {}
|
||||
|
||||
tapable@2.3.3: {}
|
||||
|
||||
tar-stream@3.2.0:
|
||||
dependencies:
|
||||
b4a: 1.8.1
|
||||
@@ -27623,7 +28094,7 @@ snapshots:
|
||||
dependencies:
|
||||
citty: 0.1.6
|
||||
defu: 6.1.7
|
||||
jiti: 2.6.1
|
||||
jiti: 2.7.0
|
||||
knitwork: 1.3.0
|
||||
scule: 1.3.0
|
||||
|
||||
@@ -28012,7 +28483,7 @@ snapshots:
|
||||
acorn-import-phases: 1.0.4(acorn@8.16.0)
|
||||
browserslist: 4.28.2
|
||||
chrome-trace-event: 1.0.4
|
||||
enhanced-resolve: 5.20.1
|
||||
enhanced-resolve: 5.22.0
|
||||
es-module-lexer: 2.1.0
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
@@ -28022,7 +28493,7 @@ snapshots:
|
||||
mime-db: 1.54.0
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 4.3.3
|
||||
tapable: 2.3.2
|
||||
tapable: 2.3.3
|
||||
terser-webpack-plugin: 5.5.0(webpack@5.106.2)
|
||||
watchpack: 2.5.1
|
||||
webpack-sources: 3.4.1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"$schema": "https://turborepo.dev/schema.json",
|
||||
"globalDependencies": [".env"],
|
||||
"globalEnv": [
|
||||
"CI",
|
||||
"DATABASE_URL",
|
||||
|
||||
Reference in New Issue
Block a user