docs/solutions/developer-experience/2026-05-24-shadcn-v4-registry-schema-needs-source-only-validation.md
Shadcn v4 moved the registry schema and registry item types to the shadcn/schema export. Keeping Plate on [email protected] and shadcn/registry meant the docs migration could look aligned with upstream while still compiling against the old registry contract.
Plate also cannot use a local registry build as the normal proof point, because registry build output is CI-owned in this repo.
Registry, RegistryItem, registryItemSchema, and registryItemFileSchema from shadcn/registry.apps/www/package.json still pinned shadcn to 2.6.3, while the upstream comparison target uses [email protected].build-registry.mts validated only the item array, not the full registry shape that shadcn v4 validates.@plate/* dependency specifiers align installer behavior, but they do not prove the package-level schema contract is v4.registry export directly. build-registry.mts adds plate-ui to block dependencies before validation, so the check has to mirror the builder composition.Upgrade apps/www to the upstream shadcn package version and move schema/type imports to the v4 export:
import {
type Registry,
type RegistryItem,
registrySchema,
} from 'shadcn/schema';
Validate the full generated registry object in the registry builders:
const registry: Registry = registrySchema.parse({
name: 'plate',
homepage: 'https://platejs.org',
items: registryItems.map((item) => ({
...item,
registryDependencies: item.registryDependencies?.map(
toRegistryDependencySpecifier
),
})),
});
Add a source-only check that mirrors the builder's authored registry composition without writing generated output:
const normalizedRegistry = registrySchema.parse({
homepage: 'https://platejs.org',
name: 'plate',
items: [
...registryInit,
...registryUI,
...registryComponents,
...registryBlocks.map((block) => ({
...block,
registryDependencies: ['plate-ui', ...(block.registryDependencies ?? [])],
})),
...registryLib,
...registryStyles,
...registryHooks,
...registryExamples,
].map((item) => ({
...item,
registryDependencies: item.registryDependencies?.map(
toRegistryDependencySpecifier
),
})),
});
Wire that check into apps/www typecheck so every PR validates the v4 contract:
{
"typecheck": "pnpm build:source && tsx --tsconfig ./scripts/tsconfig.scripts.json scripts/check-docs-source-parity.mts && tsx --tsconfig ./scripts/tsconfig.scripts.json scripts/check-registry-source.mts && tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.package-integration.json"
}
The shadcn/schema export is the v4 registry data contract. Moving imports there makes TypeScript and runtime validation fail if Plate drifts back to the old package shape.
registrySchema.parse validates the same top-level registry shape that the CLI consumes: name, homepage, and validated items. The source-only check gives local confidence without generating or committing registry output.
Mirroring the builder's block dependency injection matters because raw registry source and emitted registry payload are not identical. The contract that users and templates receive is the normalized builder output.
shadcn/schema, not shadcn/registry.build:registry as the everyday proof.rg -n 'shadcn/registry' apps/www --glob '!apps/www/public/**'