From 325c627cb26e552685ead5e0ddf69b842c1db9ec Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 14:50:46 -1000 Subject: [PATCH 01/13] chore(deps): update TypeScript and MSW --- package-lock.json | 68 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52001d64..b6e346dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,14 +25,14 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-tsdoc": "^0.2.17", - "msw": "^0.49.3", + "msw": "^1.2.1", "node-fetch": "^3.3.0", "prettier": "^2.8.3", "prettier-plugin-jsdoc": "^0.4.2", "size-limit": "^8.1.2", "standard-version": "^9.5.0", "ts-expect": "^1.3.0", - "typescript": "^4.9.4", + "typescript": "^5.0.4", "vite": "^4.0.4", "vite-plugin-sdk": "^0.1.0", "vitest": "^0.27.2" @@ -4391,9 +4391,9 @@ } }, "node_modules/is-node-process": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz", - "integrity": "sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true }, "node_modules/is-number": { @@ -5660,9 +5660,9 @@ "dev": true }, "node_modules/msw": { - "version": "0.49.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", - "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", + "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5675,12 +5675,12 @@ "chokidar": "^3.4.2", "cookie": "^0.4.2", "graphql": "^15.0.0 || ^16.0.0", - "headers-polyfill": "^3.1.0", + "headers-polyfill": "^3.1.2", "inquirer": "^8.2.0", - "is-node-process": "^1.0.1", + "is-node-process": "^1.2.0", "js-levenshtein": "^1.1.6", "node-fetch": "^2.6.7", - "outvariant": "^1.3.0", + "outvariant": "^1.4.0", "path-to-regexp": "^6.2.0", "strict-event-emitter": "^0.4.3", "type-fest": "^2.19.0", @@ -5697,7 +5697,7 @@ "url": "https://opencollective.com/mswjs" }, "peerDependencies": { - "typescript": ">= 4.4.x <= 4.9.x" + "typescript": ">= 4.4.x <= 5.0.x" }, "peerDependenciesMeta": { "typescript": { @@ -6006,9 +6006,9 @@ } }, "node_modules/outvariant": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz", - "integrity": "sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", + "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", "dev": true }, "node_modules/p-limit": { @@ -7307,16 +7307,16 @@ "dev": true }, "node_modules/typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=12.20" } }, "node_modules/ufo": { @@ -11378,9 +11378,9 @@ "dev": true }, "is-node-process": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz", - "integrity": "sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", "dev": true }, "is-number": { @@ -12242,9 +12242,9 @@ "dev": true }, "msw": { - "version": "0.49.3", - "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", - "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", + "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", "dev": true, "requires": { "@mswjs/cookies": "^0.2.2", @@ -12256,12 +12256,12 @@ "chokidar": "^3.4.2", "cookie": "^0.4.2", "graphql": "^15.0.0 || ^16.0.0", - "headers-polyfill": "^3.1.0", + "headers-polyfill": "^3.1.2", "inquirer": "^8.2.0", - "is-node-process": "^1.0.1", + "is-node-process": "^1.2.0", "js-levenshtein": "^1.1.6", "node-fetch": "^2.6.7", - "outvariant": "^1.3.0", + "outvariant": "^1.4.0", "path-to-regexp": "^6.2.0", "strict-event-emitter": "^0.4.3", "type-fest": "^2.19.0", @@ -12486,9 +12486,9 @@ "dev": true }, "outvariant": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz", - "integrity": "sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", + "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", "dev": true }, "p-limit": { @@ -13458,9 +13458,9 @@ "dev": true }, "typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", + "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "dev": true }, "ufo": { diff --git a/package.json b/package.json index 3f061cc9..d9534fa8 100644 --- a/package.json +++ b/package.json @@ -69,14 +69,14 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-tsdoc": "^0.2.17", - "msw": "^0.49.3", + "msw": "^1.2.1", "node-fetch": "^3.3.0", "prettier": "^2.8.3", "prettier-plugin-jsdoc": "^0.4.2", "size-limit": "^8.1.2", "standard-version": "^9.5.0", "ts-expect": "^1.3.0", - "typescript": "^4.9.4", + "typescript": "^5.0.4", "vite": "^4.0.4", "vite-plugin-sdk": "^0.1.0", "vitest": "^0.27.2" From e10273803338110c417f53037908f6c76d89714e Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 14:51:09 -1000 Subject: [PATCH 02/13] test: fix `asLink` tests --- test/helpers-asLink.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/helpers-asLink.test.ts b/test/helpers-asLink.test.ts index 6e68f0b5..b7f779ee 100644 --- a/test/helpers-asLink.test.ts +++ b/test/helpers-asLink.test.ts @@ -110,7 +110,7 @@ it("resolves a link to web field", () => { url: "https://prismic.io", }; - expect(asLink(field, linkResolver), "https://prismic.io"); + expect(asLink(field, linkResolver)).toBe("https://prismic.io"); }); it("resolves a link to media field", () => { @@ -124,11 +124,11 @@ it("resolves a link to media field", () => { width: "42", }; - expect(asLink(field, linkResolver), "https://prismic.io"); + expect(asLink(field, linkResolver)).toBe("https://prismic.io"); }); it("resolves a document", () => { const document = { ...documentFixture.empty }; - expect(asLink(document), "/test"); + expect(asLink(document)).toBe("/test"); }); From 0ab9cba3815ebbdac7d21f50dab39653fce8fc2a Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 14:51:23 -1000 Subject: [PATCH 03/13] fix: remove incorrect type from asLink's return type --- src/helpers/asLink.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 8909e63b..32e78526 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -34,7 +34,7 @@ type AsLinkReturnType< | FilledLinkToMediaField | FilledContentRelationshipField | PrismicDocument - ? LinkResolverFunctionReturnType | string | null + ? LinkResolverFunctionReturnType | null : null; /** From c68c59568e5ad0b2e8e32af99153593c0f686f9f Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 14:51:51 -1000 Subject: [PATCH 04/13] feat: add `asLinkAttrs()` helper --- src/helpers/asLinkAttrs.ts | 62 +++++++++++ src/index.ts | 1 + test/helpers-asLinkAttrs.test.ts | 186 +++++++++++++++++++++++++++++++ 3 files changed, 249 insertions(+) create mode 100644 src/helpers/asLinkAttrs.ts create mode 100644 test/helpers-asLinkAttrs.test.ts diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts new file mode 100644 index 00000000..a170342b --- /dev/null +++ b/src/helpers/asLinkAttrs.ts @@ -0,0 +1,62 @@ +import type { FilledContentRelationshipField } from "../types/value/contentRelationship"; +import type { PrismicDocument } from "../types/value/document"; +import { FilledLinkToWebField, LinkField } from "../types/value/link"; +import type { FilledLinkToMediaField } from "../types/value/linkToMedia"; + +import { LinkResolverFunction, asLink } from "./asLink"; +import { link as isFilledLink } from "./isFilled"; + +type AsLinkAttrsReturnType< + LinkResolverFunctionReturnType = ReturnType, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, +> = Field extends + | FilledLinkToWebField + | FilledLinkToMediaField + | FilledContentRelationshipField + | PrismicDocument + ? { + href: LinkResolverFunctionReturnType | undefined; + target?: string; + rel?: string; + } + : { + href?: undefined; + target?: undefined; + rel?: undefined; + }; + +export const asLinkAttrs = < + LinkResolverFunctionReturnType = ReturnType, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, +>( + linkFieldOrDocument: Field, + linkResolver?: LinkResolverFunction | null, +): AsLinkAttrsReturnType => { + if ( + linkFieldOrDocument && + ("link_type" in linkFieldOrDocument + ? isFilledLink(linkFieldOrDocument) + : linkFieldOrDocument) + ) { + const target = + "target" in linkFieldOrDocument ? linkFieldOrDocument.target : undefined; + + const href = asLink(linkFieldOrDocument, linkResolver); + + return { + href: href == null ? undefined : href, + target, + rel: target === "_blank" ? "noopener noreferrer" : undefined, + }; + } + + return {}; +}; diff --git a/src/index.ts b/src/index.ts index d46ee040..ea907ec4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,6 +59,7 @@ export type { // Primary Helpers API. export { asDate } from "./helpers/asDate"; export { asLink } from "./helpers/asLink"; +export { asLinkAttrs } from "./helpers/asLinkAttrs"; export { asText } from "./helpers/asText"; export { asHTML } from "./helpers/asHTML"; export { asImageSrc } from "./helpers/asImageSrc"; diff --git a/test/helpers-asLinkAttrs.test.ts b/test/helpers-asLinkAttrs.test.ts new file mode 100644 index 00000000..713cd9c7 --- /dev/null +++ b/test/helpers-asLinkAttrs.test.ts @@ -0,0 +1,186 @@ +import { expect, it } from "vitest"; + +import { documentFixture } from "./__fixtures__/document"; +import { linkResolver } from "./__fixtures__/linkResolver"; + +import { LinkType, asLinkAttrs } from "../src"; + +it("returns null for nullish inputs", () => { + expect(asLinkAttrs(null, linkResolver)).toEqual({}); + expect(asLinkAttrs(undefined, linkResolver)).toEqual({}); +}); + +it("returns null when link to document field is empty", () => { + const field = { + link_type: LinkType.Document, + }; + + expect(asLinkAttrs(field, linkResolver)).toEqual({}); +}); + +it("returns null when link to media field is empty", () => { + const field = { + link_type: LinkType.Media, + }; + + expect(asLinkAttrs(field, linkResolver)).toEqual({}); +}); + +it("returns null when link field is empty", () => { + const field = { + link_type: LinkType.Any, + }; + + expect(asLinkAttrs(field, linkResolver)).toEqual({}); +}); + +it("resolves a link to document field without Route Resolver", () => { + const field = { + id: "XvoFFREAAM0WGBng", + type: "page", + tags: [], + slug: "slug", + lang: "en-us", + uid: "test", + link_type: LinkType.Document, + isBroken: false, + }; + + expect( + asLinkAttrs(field), + "returns null if both Link Resolver and Route Resolver are not used", + ).toEqual({}); + expect( + asLinkAttrs(field, linkResolver), + "uses Link Resolver URL if Link Resolver returns a non-nullish value", + ).toEqual({ + href: "/test", + target: undefined, + rel: undefined, + }); + expect( + asLinkAttrs(field, () => undefined), + "returns null if Link Resolver returns undefined", + ).toEqual({}); + expect( + asLinkAttrs(field, () => null), + "returns null if Link Resolver returns null", + ).toEqual({}); +}); + +it("resolves a link to document field with Route Resolver", () => { + const field = { + id: "XvoFFREAAM0WGBng", + type: "page", + tags: [], + slug: "slug", + lang: "en-us", + uid: "uid", + url: "url", + link_type: LinkType.Document, + isBroken: false, + }; + + expect( + asLinkAttrs(field), + "uses Route Resolver URL if Link Resolver is not given", + ).toEqual({ + href: field.url, + target: undefined, + rel: undefined, + }); + expect( + asLinkAttrs(field, () => "link-resolver-value"), + "uses Link Resolver URL if Link Resolver returns a non-nullish value", + ).toEqual({ + href: "link-resolver-value", + target: undefined, + rel: undefined, + }); + expect( + asLinkAttrs(field, () => undefined), + "uses Route Resolver URL if Link Resolver returns undefined", + ).toEqual({ + href: field.url, + target: undefined, + rel: undefined, + }); + expect( + asLinkAttrs(field, () => null), + "uses Route Resolver URL if Link Resolver returns null", + ).toEqual({ + href: field.url, + target: undefined, + rel: undefined, + }); +}); + +it("returns null when given a document field and linkResolver is not provided ", () => { + const field = { + id: "XvoFFREAAM0WGBng", + link_type: LinkType.Document, + }; + + expect(asLinkAttrs(field)).toEqual({}); +}); + +it("resolves a link to web field", () => { + const field = { + link_type: LinkType.Web, + url: "https://prismic.io", + }; + + expect(asLinkAttrs(field, linkResolver)).toEqual({ + href: "https://prismic.io", + target: undefined, + rel: undefined, + }); +}); + +it("returns correct target when field has a target", () => { + const field = { + link_type: LinkType.Web, + url: "https://prismic.io", + target: "_blank", + }; + + expect(asLinkAttrs(field, linkResolver).target).toBe(field.target); +}); + +it('returns "noopener noreferrer" rel value when the field\'s target is "_blank"', () => { + const field = { + link_type: LinkType.Web, + url: "https://prismic.io", + target: "_blank", + }; + + expect(asLinkAttrs(field, linkResolver).rel).toBe("noopener noreferrer"); +}); + +it("resolves a link to media field", () => { + const field = { + link_type: LinkType.Media, + name: "test.jpg", + kind: "image", + url: "https://prismic.io", + size: "420", + height: "42", + width: "42", + }; + + expect(asLinkAttrs(field, linkResolver)).toEqual({ + href: "https://prismic.io", + target: undefined, + rel: undefined, + }); +}); + +it("resolves a document", () => { + const document = { ...documentFixture.empty }; + + expect(asLinkAttrs(document)).toEqual({ + href: "/test", + target: undefined, + rel: undefined, + }); +}); From d3361a1af1617418704d4915a64a4a8fd9705158 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 14:57:55 -1000 Subject: [PATCH 05/13] docs: add docs for `asLinkAttrs()` and refresh docs for `asLink()` --- src/helpers/asLink.ts | 12 ++++++------ src/helpers/asLinkAttrs.ts | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 32e78526..6a06503c 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -38,16 +38,16 @@ type AsLinkReturnType< : null; /** - * Resolves any type of Link Field or document to a URL + * Resolves any type of Link field or Prismic document to a URL. * - * @typeParam LinkResolverFunctionReturnType - Link resolver function return + * @typeParam LinkResolverFunctionReturnType - Link Resolver function return * type - * @param linkFieldOrDocument - Any kind of Link Field or a document to resolve - * @param linkResolver - An optional link resolver function, without it you're + * @param linkFieldOrDocument - Any kind of Link field or a document to resolve + * @param linkResolver - An optional Link Resolver function. Without it, you are * expected to use the `routes` options from the API * - * @returns Resolved URL, null if provided link is empty - * @see Prismic link resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} + * @returns Resolved URL or, if the provided Link field or document is empty, `null` + * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} * @see Prismic API `routes` options documentation: {@link https://prismic.io/docs/route-resolver} */ export const asLink = < diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts index a170342b..a4067e41 100644 --- a/src/helpers/asLinkAttrs.ts +++ b/src/helpers/asLinkAttrs.ts @@ -6,6 +6,9 @@ import type { FilledLinkToMediaField } from "../types/value/linkToMedia"; import { LinkResolverFunction, asLink } from "./asLink"; import { link as isFilledLink } from "./isFilled"; +/** + * The return type of `asLinkAttrs()`. + */ type AsLinkAttrsReturnType< LinkResolverFunctionReturnType = ReturnType, Field extends LinkField | PrismicDocument | null | undefined = @@ -29,6 +32,21 @@ type AsLinkAttrsReturnType< rel?: undefined; }; +/** + * Resolves any type of Link field or Prismic document to a set of link attributes. The attributes are designed to be passed to link HTML elements, like ``. + * + * If a Link field is configured to open its link in a new tab, `rel` is returned as `"noopener noreferrer"`. + * + * @typeParam LinkResolverFunctionReturnType - Link Resolver function return + * type + * @param linkFieldOrDocument - Any kind of Link field or a document to resolve + * @param linkResolver - An optional Link Resolver function. Without it, you are + * expected to use the `routes` options from the API + * + * @returns Resolved set of link attributes or, if the provided Link field or document is empty, and empty object + * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} + * @see Prismic API `routes` options documentation: {@link https://prismic.io/docs/route-resolver} + */ export const asLinkAttrs = < LinkResolverFunctionReturnType = ReturnType, Field extends LinkField | PrismicDocument | null | undefined = From 31225b3a8c7e9708c4a9b193c159b72e6b42f9ac Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:06:21 -1000 Subject: [PATCH 06/13] refactor: remove `@ts-expect-error` --- src/helpers/asLink.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 6a06503c..46900ab4 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -66,16 +66,11 @@ export const asLink = < } // Converts document to Link Field if needed - const linkField = - // prettier-ignore - ( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - Bug in TypeScript 4.9: https://github.com/microsoft/TypeScript/issues/51501 - // TODO: Remove the `prettier-ignore` comment when this bug is fixed. - "link_type" in linkFieldOrDocument - ? linkFieldOrDocument - : documentToLinkField(linkFieldOrDocument) - ) as LinkField; + const linkField = ( + "link_type" in linkFieldOrDocument + ? linkFieldOrDocument + : documentToLinkField(linkFieldOrDocument) + ) as LinkField; switch (linkField.link_type) { case LinkType.Media: From 1aa699b775b5c3f3a7f5b96835719375cf640a06 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:08:48 -1000 Subject: [PATCH 07/13] test: fix MSW type --- test/__setup__.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/__setup__.ts b/test/__setup__.ts index 5e6434eb..069b1aee 100644 --- a/test/__setup__.ts +++ b/test/__setup__.ts @@ -2,14 +2,14 @@ import { afterAll, beforeAll, beforeEach, vi } from "vitest"; import { MockFactory, createMockFactory } from "@prismicio/mock"; import AbortController from "abort-controller"; -import { SetupServerApi, setupServer } from "msw/node"; +import { SetupServer, setupServer } from "msw/node"; import * as prismic from "../src"; declare module "vitest" { export interface TestContext { mock: MockFactory; - server: SetupServerApi; + server: SetupServer; } } From 921937c58708b3921815d8355b67125855f0a742 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:12:12 -1000 Subject: [PATCH 08/13] Revert "chore(deps): update TypeScript and MSW" This reverts commit 325c627cb26e552685ead5e0ddf69b842c1db9ec. --- package-lock.json | 68 +++++++++++++++++++++++------------------------ package.json | 4 +-- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6e346dc..52001d64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,14 +25,14 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-tsdoc": "^0.2.17", - "msw": "^1.2.1", + "msw": "^0.49.3", "node-fetch": "^3.3.0", "prettier": "^2.8.3", "prettier-plugin-jsdoc": "^0.4.2", "size-limit": "^8.1.2", "standard-version": "^9.5.0", "ts-expect": "^1.3.0", - "typescript": "^5.0.4", + "typescript": "^4.9.4", "vite": "^4.0.4", "vite-plugin-sdk": "^0.1.0", "vitest": "^0.27.2" @@ -4391,9 +4391,9 @@ } }, "node_modules/is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz", + "integrity": "sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==", "dev": true }, "node_modules/is-number": { @@ -5660,9 +5660,9 @@ "dev": true }, "node_modules/msw": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", - "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "version": "0.49.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", + "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", "dev": true, "hasInstallScript": true, "dependencies": { @@ -5675,12 +5675,12 @@ "chokidar": "^3.4.2", "cookie": "^0.4.2", "graphql": "^15.0.0 || ^16.0.0", - "headers-polyfill": "^3.1.2", + "headers-polyfill": "^3.1.0", "inquirer": "^8.2.0", - "is-node-process": "^1.2.0", + "is-node-process": "^1.0.1", "js-levenshtein": "^1.1.6", "node-fetch": "^2.6.7", - "outvariant": "^1.4.0", + "outvariant": "^1.3.0", "path-to-regexp": "^6.2.0", "strict-event-emitter": "^0.4.3", "type-fest": "^2.19.0", @@ -5697,7 +5697,7 @@ "url": "https://opencollective.com/mswjs" }, "peerDependencies": { - "typescript": ">= 4.4.x <= 5.0.x" + "typescript": ">= 4.4.x <= 4.9.x" }, "peerDependenciesMeta": { "typescript": { @@ -6006,9 +6006,9 @@ } }, "node_modules/outvariant": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", - "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz", + "integrity": "sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==", "dev": true }, "node_modules/p-limit": { @@ -7307,16 +7307,16 @@ "dev": true }, "node_modules/typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=12.20" + "node": ">=4.2.0" } }, "node_modules/ufo": { @@ -11378,9 +11378,9 @@ "dev": true }, "is-node-process": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", - "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.0.1.tgz", + "integrity": "sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==", "dev": true }, "is-number": { @@ -12242,9 +12242,9 @@ "dev": true }, "msw": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/msw/-/msw-1.2.1.tgz", - "integrity": "sha512-bF7qWJQSmKn6bwGYVPXOxhexTCGD5oJSZg8yt8IBClxvo3Dx/1W0zqE1nX9BSWmzRsCKWfeGWcB/vpqV6aclpw==", + "version": "0.49.3", + "resolved": "https://registry.npmjs.org/msw/-/msw-0.49.3.tgz", + "integrity": "sha512-kRCbDNbNnRq5LC1H/NUceZlrPAvSrMH6Or0mirIuH69NY84xwDruPn/hkXTovIK1KwDwbk+ZdoSyJlpiekLxEA==", "dev": true, "requires": { "@mswjs/cookies": "^0.2.2", @@ -12256,12 +12256,12 @@ "chokidar": "^3.4.2", "cookie": "^0.4.2", "graphql": "^15.0.0 || ^16.0.0", - "headers-polyfill": "^3.1.2", + "headers-polyfill": "^3.1.0", "inquirer": "^8.2.0", - "is-node-process": "^1.2.0", + "is-node-process": "^1.0.1", "js-levenshtein": "^1.1.6", "node-fetch": "^2.6.7", - "outvariant": "^1.4.0", + "outvariant": "^1.3.0", "path-to-regexp": "^6.2.0", "strict-event-emitter": "^0.4.3", "type-fest": "^2.19.0", @@ -12486,9 +12486,9 @@ "dev": true }, "outvariant": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.0.tgz", - "integrity": "sha512-AlWY719RF02ujitly7Kk/0QlV+pXGFDHrHf9O2OKqyqgBieaPOIeuSkL8sRK6j2WK+/ZAURq2kZsY0d8JapUiw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.3.0.tgz", + "integrity": "sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==", "dev": true }, "p-limit": { @@ -13458,9 +13458,9 @@ "dev": true }, "typescript": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", - "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, "ufo": { diff --git a/package.json b/package.json index d9534fa8..3f061cc9 100644 --- a/package.json +++ b/package.json @@ -69,14 +69,14 @@ "eslint-config-prettier": "^8.6.0", "eslint-plugin-prettier": "^4.2.1", "eslint-plugin-tsdoc": "^0.2.17", - "msw": "^1.2.1", + "msw": "^0.49.3", "node-fetch": "^3.3.0", "prettier": "^2.8.3", "prettier-plugin-jsdoc": "^0.4.2", "size-limit": "^8.1.2", "standard-version": "^9.5.0", "ts-expect": "^1.3.0", - "typescript": "^5.0.4", + "typescript": "^4.9.4", "vite": "^4.0.4", "vite-plugin-sdk": "^0.1.0", "vitest": "^0.27.2" From fea5325c09087a110351243d4354a7c073e52602 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:13:45 -1000 Subject: [PATCH 09/13] fix: restore `@ts-expect-error` --- src/helpers/asLink.ts | 15 ++++++++++----- src/helpers/asLinkAttrs.ts | 4 ++++ test/__setup__.ts | 4 ++-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 46900ab4..6a06503c 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -66,11 +66,16 @@ export const asLink = < } // Converts document to Link Field if needed - const linkField = ( - "link_type" in linkFieldOrDocument - ? linkFieldOrDocument - : documentToLinkField(linkFieldOrDocument) - ) as LinkField; + const linkField = + // prettier-ignore + ( + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Bug in TypeScript 4.9: https://github.com/microsoft/TypeScript/issues/51501 + // TODO: Remove the `prettier-ignore` comment when this bug is fixed. + "link_type" in linkFieldOrDocument + ? linkFieldOrDocument + : documentToLinkField(linkFieldOrDocument) + ) as LinkField; switch (linkField.link_type) { case LinkType.Media: diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts index a4067e41..856cbdec 100644 --- a/src/helpers/asLinkAttrs.ts +++ b/src/helpers/asLinkAttrs.ts @@ -60,11 +60,15 @@ export const asLinkAttrs = < ): AsLinkAttrsReturnType => { if ( linkFieldOrDocument && + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Bug in TypeScript 4.9: https://github.com/microsoft/TypeScript/issues/51501 ("link_type" in linkFieldOrDocument ? isFilledLink(linkFieldOrDocument) : linkFieldOrDocument) ) { const target = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - Bug in TypeScript 4.9: https://github.com/microsoft/TypeScript/issues/51501 "target" in linkFieldOrDocument ? linkFieldOrDocument.target : undefined; const href = asLink(linkFieldOrDocument, linkResolver); diff --git a/test/__setup__.ts b/test/__setup__.ts index 069b1aee..5e6434eb 100644 --- a/test/__setup__.ts +++ b/test/__setup__.ts @@ -2,14 +2,14 @@ import { afterAll, beforeAll, beforeEach, vi } from "vitest"; import { MockFactory, createMockFactory } from "@prismicio/mock"; import AbortController from "abort-controller"; -import { SetupServer, setupServer } from "msw/node"; +import { SetupServerApi, setupServer } from "msw/node"; import * as prismic from "../src"; declare module "vitest" { export interface TestContext { mock: MockFactory; - server: SetupServer; + server: SetupServerApi; } } From ee069f22fea62b0390fae0c5686baa58f1bf0207 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:26:14 -1000 Subject: [PATCH 10/13] fix: update types for TS backwards compatibility --- src/helpers/asLink.ts | 2 +- src/helpers/asLinkAttrs.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 6a06503c..4b7e4369 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -34,7 +34,7 @@ type AsLinkReturnType< | FilledLinkToMediaField | FilledContentRelationshipField | PrismicDocument - ? LinkResolverFunctionReturnType | null + ? LinkResolverFunctionReturnType | string | null : null; /** diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts index 856cbdec..a946d30a 100644 --- a/src/helpers/asLinkAttrs.ts +++ b/src/helpers/asLinkAttrs.ts @@ -22,7 +22,7 @@ type AsLinkAttrsReturnType< | FilledContentRelationshipField | PrismicDocument ? { - href: LinkResolverFunctionReturnType | undefined; + href: NonNullable | undefined; target?: string; rel?: string; } @@ -74,7 +74,9 @@ export const asLinkAttrs = < const href = asLink(linkFieldOrDocument, linkResolver); return { - href: href == null ? undefined : href, + href: (href == null + ? undefined + : href) as AsLinkAttrsReturnType["href"], target, rel: target === "_blank" ? "noopener noreferrer" : undefined, }; From 32746bfd4ecda6e0d7249d1d5a36607890633c68 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Fri, 7 Apr 2023 15:34:48 -1000 Subject: [PATCH 11/13] test: fix test names --- test/helpers-asLinkAttrs.test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/helpers-asLinkAttrs.test.ts b/test/helpers-asLinkAttrs.test.ts index 713cd9c7..a4ad8198 100644 --- a/test/helpers-asLinkAttrs.test.ts +++ b/test/helpers-asLinkAttrs.test.ts @@ -5,12 +5,12 @@ import { linkResolver } from "./__fixtures__/linkResolver"; import { LinkType, asLinkAttrs } from "../src"; -it("returns null for nullish inputs", () => { +it("returns empty object for nullish inputs", () => { expect(asLinkAttrs(null, linkResolver)).toEqual({}); expect(asLinkAttrs(undefined, linkResolver)).toEqual({}); }); -it("returns null when link to document field is empty", () => { +it("returns empty object when link to document field is empty", () => { const field = { link_type: LinkType.Document, }; @@ -18,7 +18,7 @@ it("returns null when link to document field is empty", () => { expect(asLinkAttrs(field, linkResolver)).toEqual({}); }); -it("returns null when link to media field is empty", () => { +it("returns empty object when link to media field is empty", () => { const field = { link_type: LinkType.Media, }; @@ -26,7 +26,7 @@ it("returns null when link to media field is empty", () => { expect(asLinkAttrs(field, linkResolver)).toEqual({}); }); -it("returns null when link field is empty", () => { +it("returns empty object when link field is empty", () => { const field = { link_type: LinkType.Any, }; @@ -48,7 +48,7 @@ it("resolves a link to document field without Route Resolver", () => { expect( asLinkAttrs(field), - "returns null if both Link Resolver and Route Resolver are not used", + "returns empty object if both Link Resolver and Route Resolver are not used", ).toEqual({}); expect( asLinkAttrs(field, linkResolver), @@ -60,11 +60,11 @@ it("resolves a link to document field without Route Resolver", () => { }); expect( asLinkAttrs(field, () => undefined), - "returns null if Link Resolver returns undefined", + "returns empty object if Link Resolver returns undefined", ).toEqual({}); expect( asLinkAttrs(field, () => null), - "returns null if Link Resolver returns null", + "returns empty object if Link Resolver returns null", ).toEqual({}); }); @@ -115,7 +115,7 @@ it("resolves a link to document field with Route Resolver", () => { }); }); -it("returns null when given a document field and linkResolver is not provided ", () => { +it("returns empty object when given a document field and linkResolver is not provided ", () => { const field = { id: "XvoFFREAAM0WGBng", link_type: LinkType.Document, From df04abe96cdab8e7624d640720c5366d519de42a Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 10 Apr 2023 11:47:07 -1000 Subject: [PATCH 12/13] feat: re-work `asLinkAttrs()` to use new `rel` logic --- src/helpers/asLink.ts | 2 +- src/helpers/asLinkAttrs.ts | 56 ++++++- src/lib/isInternalURL.ts | 14 ++ test/helpers-asLinkAttrs.test.ts | 243 ++++++++++++++----------------- 4 files changed, 173 insertions(+), 142 deletions(-) create mode 100644 src/lib/isInternalURL.ts diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 4b7e4369..08ceb794 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -22,7 +22,7 @@ export type LinkResolverFunction = ( /** * The return type of `asLink()`. */ -type AsLinkReturnType< +export type AsLinkReturnType< LinkResolverFunctionReturnType = string | null | undefined, Field extends LinkField | PrismicDocument | null | undefined = | LinkField diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts index a946d30a..af5c8ec8 100644 --- a/src/helpers/asLinkAttrs.ts +++ b/src/helpers/asLinkAttrs.ts @@ -1,11 +1,42 @@ +import { isInternalURL } from "../lib/isInternalURL"; + import type { FilledContentRelationshipField } from "../types/value/contentRelationship"; import type { PrismicDocument } from "../types/value/document"; import { FilledLinkToWebField, LinkField } from "../types/value/link"; import type { FilledLinkToMediaField } from "../types/value/linkToMedia"; -import { LinkResolverFunction, asLink } from "./asLink"; +import { AsLinkReturnType, LinkResolverFunction, asLink } from "./asLink"; import { link as isFilledLink } from "./isFilled"; +type AsLinkAttrsConfigRelArgs< + LinkResolverFunctionReturnType = ReturnType, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, +> = { + href: + | NonNullable> + | undefined; + isExternal: boolean; + target?: string; +}; + +type AsLinkAttrsConfig< + LinkResolverFunctionReturnType = ReturnType, + Field extends LinkField | PrismicDocument | null | undefined = + | LinkField + | PrismicDocument + | null + | undefined, +> = { + linkResolver?: LinkResolverFunction; + rel?: ( + args: AsLinkAttrsConfigRelArgs, + ) => string | undefined | void; +}; + /** * The return type of `asLinkAttrs()`. */ @@ -22,7 +53,9 @@ type AsLinkAttrsReturnType< | FilledContentRelationshipField | PrismicDocument ? { - href: NonNullable | undefined; + href: + | NonNullable> + | undefined; target?: string; rel?: string; } @@ -40,8 +73,7 @@ type AsLinkAttrsReturnType< * @typeParam LinkResolverFunctionReturnType - Link Resolver function return * type * @param linkFieldOrDocument - Any kind of Link field or a document to resolve - * @param linkResolver - An optional Link Resolver function. Without it, you are - * expected to use the `routes` options from the API + * @param config - Configuration that determines the output of `asLinkAttrs()` * * @returns Resolved set of link attributes or, if the provided Link field or document is empty, and empty object * @see Prismic Link Resolver documentation: {@link https://prismic.io/docs/route-resolver#link-resolver} @@ -56,7 +88,7 @@ export const asLinkAttrs = < | undefined, >( linkFieldOrDocument: Field, - linkResolver?: LinkResolverFunction | null, + config: AsLinkAttrsConfig = {}, ): AsLinkAttrsReturnType => { if ( linkFieldOrDocument && @@ -71,14 +103,24 @@ export const asLinkAttrs = < // @ts-ignore - Bug in TypeScript 4.9: https://github.com/microsoft/TypeScript/issues/51501 "target" in linkFieldOrDocument ? linkFieldOrDocument.target : undefined; - const href = asLink(linkFieldOrDocument, linkResolver); + const rawHref = asLink(linkFieldOrDocument, config.linkResolver); + const href = + rawHref == null ? undefined : (rawHref as NonNullable); + + const isExternal = typeof href === "string" ? !isInternalURL(href) : false; + + const rel = config.rel + ? config.rel({ href, isExternal, target }) + : isExternal + ? "noreferrer" + : undefined; return { href: (href == null ? undefined : href) as AsLinkAttrsReturnType["href"], target, - rel: target === "_blank" ? "noopener noreferrer" : undefined, + rel: rel == null ? undefined : rel, }; } diff --git a/src/lib/isInternalURL.ts b/src/lib/isInternalURL.ts new file mode 100644 index 00000000..c1bb6fa8 --- /dev/null +++ b/src/lib/isInternalURL.ts @@ -0,0 +1,14 @@ +/** + * Determines if a URL is internal or external. + * + * @param url - The URL to check if internal or external. + * + * @returns `true` if `url` is internal, `false` otherwise. + */ +// TODO: This does not detect all relative URLs as internal such as `about` or `./about`. This function assumes relative URLs start with a "/" or "#"`. +export const isInternalURL = (url: string): boolean => { + const isInternal = /^(\/(?!\/)|#)/.test(url); + const isSpecialLink = !isInternal && !/^https?:\/\//.test(url); + + return isInternal && !isSpecialLink; +}; diff --git a/test/helpers-asLinkAttrs.test.ts b/test/helpers-asLinkAttrs.test.ts index a4ad8198..4b334ce5 100644 --- a/test/helpers-asLinkAttrs.test.ts +++ b/test/helpers-asLinkAttrs.test.ts @@ -1,186 +1,161 @@ -import { expect, it } from "vitest"; +import { expect, it, vi } from "vitest"; -import { documentFixture } from "./__fixtures__/document"; -import { linkResolver } from "./__fixtures__/linkResolver"; - -import { LinkType, asLinkAttrs } from "../src"; +import { asLinkAttrs } from "../src"; it("returns empty object for nullish inputs", () => { - expect(asLinkAttrs(null, linkResolver)).toEqual({}); - expect(asLinkAttrs(undefined, linkResolver)).toEqual({}); + expect(asLinkAttrs(null)).toEqual({}); + expect(asLinkAttrs(undefined)).toEqual({}); }); -it("returns empty object when link to document field is empty", () => { - const field = { - link_type: LinkType.Document, - }; +it("returns empty object when link field is empty", (ctx) => { + const field = ctx.mock.value.link({ type: "Any", state: "empty" }); - expect(asLinkAttrs(field, linkResolver)).toEqual({}); + expect(asLinkAttrs(field)).toEqual({}); }); -it("returns empty object when link to media field is empty", () => { - const field = { - link_type: LinkType.Media, - }; +it("returns empty object when link to document field is empty", (ctx) => { + const field = ctx.mock.value.link({ type: "Document", state: "empty" }); - expect(asLinkAttrs(field, linkResolver)).toEqual({}); + expect(asLinkAttrs(field)).toEqual({}); }); -it("returns empty object when link field is empty", () => { - const field = { - link_type: LinkType.Any, - }; +it("returns empty object when link to media field is empty", (ctx) => { + const field = ctx.mock.value.link({ type: "Media", state: "empty" }); - expect(asLinkAttrs(field, linkResolver)).toEqual({}); + expect(asLinkAttrs(field)).toEqual({}); }); -it("resolves a link to document field without Route Resolver", () => { - const field = { - id: "XvoFFREAAM0WGBng", - type: "page", - tags: [], - slug: "slug", - lang: "en-us", - uid: "test", - link_type: LinkType.Document, - isBroken: false, - }; - - expect( - asLinkAttrs(field), - "returns empty object if both Link Resolver and Route Resolver are not used", - ).toEqual({}); - expect( - asLinkAttrs(field, linkResolver), - "uses Link Resolver URL if Link Resolver returns a non-nullish value", - ).toEqual({ - href: "/test", +it("returns empty object when link to web field is empty", (ctx) => { + const field = ctx.mock.value.link({ type: "Web", state: "empty" }); + + expect(asLinkAttrs(field)).toEqual({}); +}); + +it("resolves a link to document field with Route Resolver", (ctx) => { + const field = ctx.mock.value.link({ type: "Document" }); + field.url = "/url"; + + expect(asLinkAttrs(field)).toEqual({ + href: field.url, + target: undefined, + rel: undefined, + }); + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" })).toEqual({ + href: "/linkResolver", target: undefined, rel: undefined, }); - expect( - asLinkAttrs(field, () => undefined), - "returns empty object if Link Resolver returns undefined", - ).toEqual({}); - expect( - asLinkAttrs(field, () => null), - "returns empty object if Link Resolver returns null", - ).toEqual({}); }); -it("resolves a link to document field with Route Resolver", () => { - const field = { - id: "XvoFFREAAM0WGBng", - type: "page", - tags: [], - slug: "slug", - lang: "en-us", - uid: "uid", - url: "url", - link_type: LinkType.Document, - isBroken: false, - }; - - expect( - asLinkAttrs(field), - "uses Route Resolver URL if Link Resolver is not given", - ).toEqual({ - href: field.url, +it("resolves a link to document field without Route Resolver", (ctx) => { + const field = ctx.mock.value.link({ type: "Document" }); + field.url = undefined; + + expect(asLinkAttrs(field)).toEqual({ + href: undefined, target: undefined, rel: undefined, }); - expect( - asLinkAttrs(field, () => "link-resolver-value"), - "uses Link Resolver URL if Link Resolver returns a non-nullish value", - ).toEqual({ - href: "link-resolver-value", + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" })).toEqual({ + href: "/linkResolver", target: undefined, rel: undefined, }); - expect( - asLinkAttrs(field, () => undefined), - "uses Route Resolver URL if Link Resolver returns undefined", - ).toEqual({ +}); + +it("resolves a link to web field", (ctx) => { + const field = ctx.mock.value.link({ type: "Web" }); + + expect(asLinkAttrs(field)).toEqual({ href: field.url, target: undefined, - rel: undefined, + rel: "noreferrer", }); - expect( - asLinkAttrs(field, () => null), - "uses Route Resolver URL if Link Resolver returns null", - ).toEqual({ + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" })).toEqual({ href: field.url, target: undefined, - rel: undefined, + rel: "noreferrer", }); }); -it("returns empty object when given a document field and linkResolver is not provided ", () => { - const field = { - id: "XvoFFREAAM0WGBng", - link_type: LinkType.Document, - }; +it("returns correct target when field has a target", (ctx) => { + const field = ctx.mock.value.link({ type: "Web", withTargetBlank: true }); - expect(asLinkAttrs(field)).toEqual({}); + expect(asLinkAttrs(field)).toEqual({ + href: field.url, + target: field.target, + rel: "noreferrer", + }); + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" })).toEqual({ + href: field.url, + target: field.target, + rel: "noreferrer", + }); }); -it("resolves a link to web field", () => { - const field = { - link_type: LinkType.Web, - url: "https://prismic.io", - }; +it("resolves a link to media field", (ctx) => { + const field = ctx.mock.value.link({ type: "Media" }); - expect(asLinkAttrs(field, linkResolver)).toEqual({ - href: "https://prismic.io", + expect(asLinkAttrs(field)).toEqual({ + href: field.url, target: undefined, - rel: undefined, + rel: "noreferrer", + }); + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" })).toEqual({ + href: field.url, + target: undefined, + rel: "noreferrer", }); }); -it("returns correct target when field has a target", () => { - const field = { - link_type: LinkType.Web, - url: "https://prismic.io", - target: "_blank", - }; +it("resolves a document", (ctx) => { + const doc = ctx.mock.value.document(); + doc.url = "/foo"; - expect(asLinkAttrs(field, linkResolver).target).toBe(field.target); + expect(asLinkAttrs(doc)).toEqual({ + href: doc.url, + target: undefined, + rel: undefined, + }); + expect(asLinkAttrs(doc, { linkResolver: () => "/linkResolver" })).toEqual({ + href: "/linkResolver", + target: undefined, + rel: undefined, + }); }); -it('returns "noopener noreferrer" rel value when the field\'s target is "_blank"', () => { - const field = { - link_type: LinkType.Web, - url: "https://prismic.io", - target: "_blank", - }; +it('returns "noreferrer" `rel` value when the field\'s `href` is external', (ctx) => { + const field = ctx.mock.value.link({ type: "Web" }); - expect(asLinkAttrs(field, linkResolver).rel).toBe("noopener noreferrer"); + expect(asLinkAttrs(field).rel).toBe("noreferrer"); + expect(asLinkAttrs(field, { linkResolver: () => "/linkResolver" }).rel).toBe( + "noreferrer", + ); }); -it("resolves a link to media field", () => { - const field = { - link_type: LinkType.Media, - name: "test.jpg", - kind: "image", - url: "https://prismic.io", - size: "420", - height: "42", - width: "42", - }; - - expect(asLinkAttrs(field, linkResolver)).toEqual({ - href: "https://prismic.io", - target: undefined, - rel: undefined, - }); -}); +it("allows the `rel` value to be configured using `config.rel`", (ctx) => { + const internalField = ctx.mock.value.link({ type: "Document" }); + internalField.url = "/foo"; + + const externalField = ctx.mock.value.link({ type: "Web" }); + externalField.url = "https://prismic.io"; + externalField.target = "_blank"; -it("resolves a document", () => { - const document = { ...documentFixture.empty }; + const relFn = vi.fn().mockImplementation(() => "bar"); - expect(asLinkAttrs(document)).toEqual({ - href: "/test", + const internalRes = asLinkAttrs(internalField, { rel: relFn }); + expect(internalRes.rel).toBe("bar"); + expect(relFn).toHaveBeenNthCalledWith(1, { + href: internalField.url, target: undefined, - rel: undefined, + isExternal: false, + }); + + const externalRes = asLinkAttrs(externalField, { rel: relFn }); + expect(externalRes.rel).toBe("bar"); + expect(relFn).toHaveBeenNthCalledWith(2, { + href: externalField.url, + target: externalField.target, + isExternal: true, }); }); From 1d9835d249c47804f3ad476f252e4470c8ef3430 Mon Sep 17 00:00:00 2001 From: Angelo Ashmore Date: Mon, 10 Apr 2023 12:06:40 -1000 Subject: [PATCH 13/13] docs: update TSDocs --- src/helpers/asLink.ts | 1 + src/helpers/asLinkAttrs.ts | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/helpers/asLink.ts b/src/helpers/asLink.ts index 08ceb794..75c096d7 100644 --- a/src/helpers/asLink.ts +++ b/src/helpers/asLink.ts @@ -42,6 +42,7 @@ export type AsLinkReturnType< * * @typeParam LinkResolverFunctionReturnType - Link Resolver function return * type + * @typeParam Field - Link field or Prismic document to resolve to a URL * @param linkFieldOrDocument - Any kind of Link field or a document to resolve * @param linkResolver - An optional Link Resolver function. Without it, you are * expected to use the `routes` options from the API diff --git a/src/helpers/asLinkAttrs.ts b/src/helpers/asLinkAttrs.ts index af5c8ec8..7317e1d9 100644 --- a/src/helpers/asLinkAttrs.ts +++ b/src/helpers/asLinkAttrs.ts @@ -68,10 +68,11 @@ type AsLinkAttrsReturnType< /** * Resolves any type of Link field or Prismic document to a set of link attributes. The attributes are designed to be passed to link HTML elements, like ``. * - * If a Link field is configured to open its link in a new tab, `rel` is returned as `"noopener noreferrer"`. + * If a resolved URL is external (i.e. starts with a protocol like `https://`), `rel` is returned as `"noreferrer"`. * * @typeParam LinkResolverFunctionReturnType - Link Resolver function return * type + * @typeParam Field - Link field or Prismic document to resolve to link attributes * @param linkFieldOrDocument - Any kind of Link field or a document to resolve * @param config - Configuration that determines the output of `asLinkAttrs()` * @@ -116,9 +117,7 @@ export const asLinkAttrs = < : undefined; return { - href: (href == null - ? undefined - : href) as AsLinkAttrsReturnType["href"], + href, target, rel: rel == null ? undefined : rel, };