Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
353 changes: 353 additions & 0 deletions rules/app-router-sdk-v2-migration.mdc
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
---
alwaysApply: true
---
# Next.js app router SDK v2 migration guide

### Install Packages

Install the following packages with npm: `npm i @uniformdev/next-app-router --save`

```jsx
"@uniformdev/next-app-router": "20.48.0",
```

It’s best to reference all three in your `package.json` to avoid version mismatches as new packages are released.

### New Middleware

Middleware is now required to route requests and evaluate compositions, the basic structure should look like this:

```jsx
import { uniformMiddleware } from '@uniformdev/next-app-router/middleware';

export default uniformMiddleware();

// IMPORTANT: runtime: "experimental-edge" is required for the middleware to work correctly for preview in Next.js 16
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|sitemap.xml|robots.txt).*)",
],
runtime: "experimental-edge",
};

```

(Optionally) You can also rewrite where the middleware will redirect to. This can be useful if you are trying to code split your components and do not want to define them all on every page.

```jsx
import { handleUniformRoute } from "@uniformdev/next-app-router/middleware";
import { NextRequest } from "next/server";

export default (request: NextRequest) => {
return handleUniformRoute({
request,
rewrite: async ({ code, route }) => {
if (route.compositionApiResponse.composition.type === "page") {
return `/rewrite-here-instead/${code}`;
}

return `/default-rewrite/${code}`;
},
});
};

```

### Server config is optional now

If you have a `uniform.server.config.ts` or `uniform.server.config.js` in the application root, you can remove it if it matches the following default:

```ts
import { UniformServerConfig } from "@uniformdev/next-app-router/config";

const config: UniformServerConfig = {
defaultConsent: true,
quirkSerialization: true,
middlewareRuntimeCache: true,
playgroundPath: "/playground",
};

export default config;
```

### New File Structure

Before the expectation was that you could have a catch all page route, now by default we expect you to have the following file structure:

- app
- uniform
- [code]
- page.tsx

The default implementation of `page.tsx` should be:

```jsx
import {
UniformComposition,
UniformPageParameters,
createUniformStaticParams,
resolveRouteFromCode,
} from "@uniformdev/next-app-router";
import { CustomUniformClientContext } from "@/components/CustomUniformClientContext";

import { resolveComponent } from "../../../components/resolveComponent";

export const generateStaticParams = async () => {
return createUniformStaticParams({
// paths: ["/"],
// Important: for localized sites, you need to add the locales to the paths
paths: ["/en"],
});
};

export default async function UniformPage(props: UniformPageParameters) {
const { code } = await props.params;
return (
<UniformComposition
code={code}
resolveRoute={resolveRouteFromCode}
resolveComponent={resolveComponent}
clientContextComponent={CustomUniformClientContext}
/>
);
}

```

## New Playground implementation

- app
- playground
- [code]
- page.tsx

The default implementation of `page.tsx` should be:

```jsx
import {
createUniformPlaygroundStaticParams,
PlaygroundParameters,
resolvePlaygroundRoute,
UniformPlayground,
} from "@uniformdev/next-app-router";

import { resolveComponent } from "@/components/resolveComponent";

export const generateStaticParams = async () => {
return createUniformPlaygroundStaticParams({
// Important: for localized sites, you need to add the locales to the paths
paths: ["/en"],
});
};

export default async function PlaygroundPage({ params }: PlaygroundParameters) {
const { code } = await params;
return (
<UniformPlayground
code={code}
resolveRoute={resolvePlaygroundRoute}
resolveComponent={resolveComponent}
/>
);
}
```

### ComponentProps

This type was reworked a little, the older interface used to look like this:

```jsx
export type ComponentProps<TProps = unknown, TSlotNames extends string = string> = TProps & {
component: ComponentInstance;
context: CompositionContext;
slots: Record<TSlotNames, SlotDefinition>;
slotName: string | undefined;
slotIndex: number | undefined;
};
```

Now it looks like this:

```
export type ComponentProps<
TParameters extends Record<string, ComponentParameter> | unknown = Record<string, ComponentParameter>,
TSlotNames extends string = string,
> = {
type: string;
variant: string | null;
slots: Record<TSlotNames, SlotDefinition>;
parameters: TParameters;
component: ComponentContext;
context: CompositionContext;
};
```

The following changes were made:

- You are no longer passed the entire component
- Parameters are no longer added to the top level of the object
- Parameters are now of type `Record<string, ComponentParameter>`

```jsx
type HeroParamters = {
title: string
}

type HeroProps = ComponentProps<HeroParameters>

const Hero = ({ title }: HeroProps) => {
return (
<h1>{title}</h1>
)
}
```

Should now be

```jsx
type HeroParamters = {
title: ComponentParameter<string>
}

type HeroProps = ComponentProps<HeroParameters>

const Hero = ({ parameters: { title }}: HeroProps) => {
return (
<h1>{title.value}</h1>
)
}
```

This change was made to keep the full parameter intact and hopefully be able to use this data to implement conditional values.

### UniformSlot

The slot definition type used to look like this:

```jsx
export type SlotDefinition = {
name: string;
items: PropsWithChildren['children'][];
};
```

Now it looks like this:

```jsx
export type SlotDefinition = {
name: string;
items: ({
_id: string;
[CANVAS_PERSONALIZATION_PARAM]: VariantMatchCriteria | undefined;
component: ReactNode;
} | null)[];
};
```

Slots can be rendered with a single prop:

```jsx
<UniformSlot slot={slots.header} />
```

If you would like to extract all of the components that make up a slot, you can do the following:

```jsx
const headerItems = slots.header.items.map((item) => item?.component);
```

If you would like to resolve the ComponentInstance data for the slot, you can use a component cache to resolve these. Currently this requires manual configuration as it wasnt working correctly when exported from the package:

Define your cache, in a file like `lib/cache.ts`

```jsx
import { createCompositionCache } from "@uniformdev/next-app-router";

export const compositionCache = createCompositionCache();
```

Pass this cache to `UniformComposition` as a prop:

```jsx
<UniformComposition
Comment thread
alexshyba marked this conversation as resolved.
{...result}
resolveComponent={resolveComponent}
compositionCache={compositionCache}
/>
```

Then in your components, you can resolve component data including components that make up a slot:

```jsx
import {
ComponentProps,
UniformSlot,
} from "@uniformdev/next-app-router/component";

import { QuirkButton } from "./custom/QuirkButton";
import { compositionCache } from "@/lib/cache";

export type PageProps = unknown;
export type PageSlots = "content" | "header" | "footer";

export const Page = ({
slots,
context,
}: ComponentProps<PageProps, PageSlots>) => {
const headerItems = slots.header.items.map((item) => {
const resolved = compositionCache.getUniformComponent({
componentId: item!._id,
compositionId: context._id,
});

return resolved;
});

return (
<div>This is a page.</div>
);
};

```

### Adapter Components

An adapter layer also exists to easy the transition from V1 to V2, use the built in `createAdapterResolveComponentFunction` function and pass the component mappings for your site.

```jsx
import { createAdapterResolveComponentFunction } from '@uniformdev/next-app-router/compat';

import * as mappings from './mappings';

export const resolveComponent = createAdapterResolveComponentFunction({ mappings });

```

Adjust your mappings to use `ResolveComponentResultWithType` and adding `mode: 'adapted'` to each definition that you are adapting.

```tsx
import type {
ComponentProps,
ResolveComponentResultWithType,
} from '@uniformdev/next-app-router/compat';

type PageComponentParameters = {
text: string;
};

const PageComponent = ({ text }: ComponentProps<PageComponentParameters>) => {
return <div>{text}</div>;
};

export const pageMapping: ResolveComponentResultWithType = {
type: 'page',
component: PageComponent,
mode: 'adapted',
};
```

Use `UniformText` and `UniformSlot` components from the same `compat` export.

### Clients

- `getDefaultRouteClient` is now `getRouteClient`
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ alwaysApply: false
- Always generate clean code and remove unused code, so the linter and typescript checks pass automatically. IMPORTANT: do not ever run build of the application as a part of the validation as this breaks developer server and is causing issues.

- Always use `pnpm` to install packages if you see `pnpm-lock.yaml` in the root folder, otherwise use `npm`. Never use `yarn`.

- You are a senior React engineer, follow best practices of React at all times.
- Use TailwindCSS best practices when building CSS. Don't re-invent the wheel.
- You are a senior engineer, do not try to impress me with over-delivery.
Expand Down
Loading