Skip to content

Commit

Permalink
feat: add initial React Server Components experiments
Browse files Browse the repository at this point in the history
  • Loading branch information
angeloashmore committed Apr 4, 2023
1 parent 29c09a4 commit 6845370
Show file tree
Hide file tree
Showing 13 changed files with 1,707 additions and 1 deletion.
21 changes: 21 additions & 0 deletions package.json
Expand Up @@ -21,12 +21,33 @@
"require": "./dist/index.cjs",
"import": "./dist/index.js"
},
"./rsc": {
"require": "./dist/rsc.cjs",
"import": "./dist/rsc.js"
},
"./config": {
"require": "./dist/rsc.cjs",
"import": "./dist/rsc.js"
},
"./package.json": "./package.json"
},
"main": "dist/index.cjs",
"module": "dist/index.js",
"react-native": "dist/index.js",
"types": "dist/index.d.ts",
"typesVersions": {
"*": {
"*": [
"dist/index.d.ts"
],
"rsc": [
"dist/rsc/index.d.ts"
],
"config": [
"dist/rsc/index.d.ts"
]
}
},
"files": [
"dist",
"src"
Expand Down
273 changes: 273 additions & 0 deletions src/rsc/PrismicLink.tsx
@@ -0,0 +1,273 @@
import * as React from "react";
import * as prismicH from "@prismicio/helpers";
import * as prismicT from "@prismicio/types";
import { config } from "@prismicio/react/config";

import { devMsg } from "../lib/devMsg";
import { isInternalURL } from "../lib/isInternalURL";
import { __PRODUCTION__ } from "../lib/__PRODUCTION__";

const voidToUndefined = <T,>(value: T): Exclude<T, void> => {
return (value === undefined ? undefined : value) as Exclude<T, void>;
};

export type LinkComponent = React.ElementType<LinkProps>;

/**
* Props provided to a component when rendered with `<PrismicLink>`.
*/
export interface LinkProps {
/**
* The URL to link.
*/
href: string;

/**
* The `target` attribute for anchor elements. If the Prismic field is
* configured to open in a new window, this prop defaults to `_blank`.
*/
target?: string;

/**
* The `rel` attribute for anchor elements. If the `target` prop is set to
* `"_blank"`, this prop defaults to `"noopener noreferrer"`.
*/
rel?: string;

/**
* Children for the component. *
*/
children?: React.ReactNode;
}

/**
* Props for `<PrismicLink>`.
*/
export type PrismicLinkProps<
InternalComponent extends React.ElementType<LinkProps> = typeof defaultInternalComponent,
ExternalComponent extends React.ElementType<LinkProps> = typeof defaultInternalComponent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
LinkResolverFunction extends prismicH.LinkResolverFunction<any> = prismicH.LinkResolverFunction,
> = Omit<
React.ComponentPropsWithoutRef<InternalComponent> &
React.ComponentPropsWithoutRef<ExternalComponent>,
keyof LinkProps
> & {
/**
* The Link Resolver used to resolve links.
*
* @remarks
* If your app uses Route Resolvers when querying for your Prismic
* repository's content, a Link Resolver does not need to be provided.
* @see Learn about Link Resolvers and Route Resolvers {@link https://prismic.io/docs/core-concepts/link-resolver-route-resolver}
*/
linkResolver?: LinkResolverFunction;

/**
* The component rendered for internal URLs. Defaults to `<a>`.
*
* If your app uses a client-side router that requires a special Link
* component, provide the Link component to this prop.
*/
internalComponent?: InternalComponent;

/**
* The component rendered for external URLs. Defaults to `<a>`.
*/
externalComponent?: ExternalComponent;

/**
* The `target` attribute for anchor elements. If the Prismic field is
* configured to open in a new window, this prop defaults to `_blank`.
*/
target?: string | null;

/**
* The `rel` attribute for anchor elements. If the `target` prop is set to
* `"_blank"`, this prop defaults to `"noopener noreferrer"`.
*/
rel?: string | null;

/**
* Children for the component. *
*/
children?: React.ReactNode;
} & (
| {
/**
* The Prismic Link field containing the URL or document to link.
*
* @see Learn about Prismic Link fields {@link https://prismic.io/docs/core-concepts/link-content-relationship}
*/
field: prismicT.LinkField | null | undefined;
}
| {
/**
* The Prismic document to link.
*/
document: prismicT.PrismicDocument | null | undefined;
}
| {
/**
* The URL to link.
*/
href: string | null | undefined;
}
);

/**
* The default component rendered for internal URLs.
*/
const defaultInternalComponent = "a";

/**
* The default component rendered for external URLs.
*/
const defaultExternalComponent = "a";

const _PrismicLink = <
InternalComponent extends React.ElementType<LinkProps> = typeof defaultInternalComponent,
ExternalComponent extends React.ElementType<LinkProps> = typeof defaultExternalComponent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
LinkResolverFunction extends prismicH.LinkResolverFunction<any> = prismicH.LinkResolverFunction,
>(
props: PrismicLinkProps<
InternalComponent,
ExternalComponent,
LinkResolverFunction
>,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ref: React.Ref<any>,
): JSX.Element | null => {
if (!__PRODUCTION__) {
if ("field" in props && props.field) {
if (!props.field.link_type) {
console.error(
`[PrismicLink] This "field" prop value caused an error to be thrown.\n`,
props.field,
);
throw new Error(
`[PrismicLink] The provided field is missing required properties to properly render a link. The link will not render. For more details, see ${devMsg(
"missing-link-properties",
)}`,
);
} else if (
Object.keys(props.field).length > 1 &&
!("url" in props.field || "uid" in props.field || "id" in props.field)
) {
console.warn(
`[PrismicLink] The provided field is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg(
"missing-link-properties",
)}`,
props.field,
);
}
} else if ("document" in props && props.document) {
if (!("url" in props.document || "id" in props.document)) {
console.warn(
`[PrismicLink] The provided document is missing required properties to properly render a link. The link may not render correctly. For more details, see ${devMsg(
"missing-link-properties",
)}`,
props.document,
);
}
}
}

const linkResolver = props.linkResolver || config.linkResolver;

let href: string | null | undefined;
if ("href" in props) {
href = props.href;
} else if ("document" in props && props.document) {
href = voidToUndefined(prismicH.asLink(props.document, linkResolver));
} else if ("field" in props && props.field) {
href = voidToUndefined(prismicH.asLink(props.field, linkResolver));
}

const isInternal = href && isInternalURL(href);

const target =
props.target ||
("field" in props &&
props.field &&
"target" in props.field &&
props.field.target) ||
undefined;

const rel =
props.rel || (target === "_blank" ? "noopener noreferrer" : undefined);

const InternalComponent: React.ElementType<LinkProps> =
props.internalComponent ||
config.internalLinkComponent ||
defaultInternalComponent;

const ExternalComponent: React.ElementType<LinkProps> =
props.externalComponent ||
config.externalLinkComponent ||
defaultExternalComponent;

const Component = isInternal ? InternalComponent : ExternalComponent;

const passthroughProps: typeof props = Object.assign({}, props);
delete passthroughProps.linkResolver;
delete passthroughProps.internalComponent;
delete passthroughProps.externalComponent;
delete passthroughProps.rel;
delete passthroughProps.target;
if ("field" in passthroughProps) {
delete passthroughProps.field;
} else if ("document" in passthroughProps) {
delete passthroughProps.document;
} else if ("href" in passthroughProps) {
delete passthroughProps.href;
}

return href ? (
<Component
// @ts-expect-error - Expression produces a union type
// that is too complex to represent. This most likely
// happens due to the polymorphic nature of this
// component, passing of "extra" props, and ref
// forwarding support.
{...passthroughProps}
ref={ref}
href={href}
target={target}
rel={rel}
/>
) : null;
};

if (!__PRODUCTION__) {
_PrismicLink.displayName = "PrismicLink";
}

/**
* React component that renders a link from a Prismic Link field.
*
* Different components can be rendered depending on whether the link is
* internal or external. This is helpful when integrating with client-side
* routers, such as a router-specific Link component.
*
* If a link is configured to open in a new window using `target="_blank"`,
* `rel="noopener noreferrer"` is set by default.
*
* @param props - Props for the component.
*
* @returns The internal or external link component depending on whether the
* link is internal or external.
*/
export const PrismicLink = React.forwardRef(_PrismicLink) as <
InternalComponent extends React.ElementType<LinkProps> = typeof defaultInternalComponent,
ExternalComponent extends React.ElementType<LinkProps> = typeof defaultExternalComponent,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
LinkResolverFunction extends prismicH.LinkResolverFunction<any> = prismicH.LinkResolverFunction,
>(
props: PrismicLinkProps<
InternalComponent,
ExternalComponent,
LinkResolverFunction
> & { ref?: React.Ref<Element> },
) => JSX.Element | null;

0 comments on commit 6845370

Please sign in to comment.