enable tsconfig strict mode & fix type errors

This commit is contained in:
James Fenn
2023-10-20 16:51:14 -04:00
parent c7f6ee4a9e
commit a9e918dc1e
57 changed files with 323 additions and 246 deletions

View File

@@ -36,19 +36,24 @@ export default defineConfig({
lastmod: new Date(), lastmod: new Date(),
i18n: { i18n: {
defaultLocale: "en", defaultLocale: "en",
locales: Object.keys(languages).reduce((prev, key) => { locales: Object.keys(languages).reduce(
(prev, key) => {
prev[key] = fileToOpenGraphConverter(key as keyof typeof languages); prev[key] = fileToOpenGraphConverter(key as keyof typeof languages);
return prev; return prev;
}, {}), },
{} as Record<string, string>,
),
}, },
filter(page) { filter(page) {
// return true, unless lart part of the URL ends with "_noindex" // return true, unless lart part of the URL ends with "_noindex"
// in which case it should not be in the sitemap // in which case it should not be in the sitemap
return !page return !(
page
.split("/") .split("/")
.filter((part) => !!part.length) .filter((part) => !!part.length)
.at(-1) .at(-1)
.endsWith("_noindex"); ?.endsWith("_noindex") ?? false
);
}, },
serialize({ url, ...rest }) { serialize({ url, ...rest }) {
return { return {

View File

@@ -58,9 +58,9 @@ async function generateCollectionEPub(
collectionPosts: PostInfo[], collectionPosts: PostInfo[],
fileLocation: string, fileLocation: string,
) { ) {
const authors = collection.authors.map((id) => { const authors = collection.authors
return getUnicornById(id, collection.locale).name; .map((id) => getUnicornById(id, collection.locale)?.name)
}); .filter((name): name is string => !!name);
const epub = new EPub( const epub = new EPub(
{ {

View File

@@ -32,7 +32,7 @@ const createPostIndex = () => {
getFn: (post) => { getFn: (post) => {
return post.authors return post.authors
.map((id) => api.getUnicornById(id, post.locale)) .map((id) => api.getUnicornById(id, post.locale))
.flatMap((author) => Object.values(author.socials)) .flatMap((author) => Object.values(author!.socials))
.join(", "); .join(", ");
}, },
weight: 1.2, weight: 1.2,
@@ -60,7 +60,7 @@ const createCollectionIndex = () => {
name: "authorName", name: "authorName",
getFn: (post) => { getFn: (post) => {
return post.authors return post.authors
.map((id) => api.getUnicornById(id, post.locale).name) .map((id) => api.getUnicornById(id, post.locale)!.name)
.join(", "); .join(", ");
}, },
weight: 1.8, weight: 1.8,
@@ -70,7 +70,7 @@ const createCollectionIndex = () => {
getFn: (post) => { getFn: (post) => {
return post.authors return post.authors
.map((id) => api.getUnicornById(id, post.locale)) .map((id) => api.getUnicornById(id, post.locale))
.flatMap((author) => Object.values(author.socials)) .flatMap((author) => Object.values(author!.socials))
.join(", "); .join(", ");
}, },
weight: 1.2, weight: 1.2,
@@ -87,12 +87,13 @@ const createCollectionIndex = () => {
const postIndex = createPostIndex(); const postIndex = createPostIndex();
const collectionIndex = createCollectionIndex(); const collectionIndex = createCollectionIndex();
const unicorns: Record<string, UnicornInfo> = api const unicorns = api.getUnicornsByLang("en").reduce(
.getUnicornsByLang("en") (obj, unicorn) => {
.reduce((obj, unicorn) => {
obj[unicorn.id] = unicorn; obj[unicorn.id] = unicorn;
return obj; return obj;
}, {}); },
{} as Record<string, UnicornInfo>,
);
const json = JSON.stringify({ const json = JSON.stringify({
postIndex, postIndex,

View File

@@ -4,8 +4,10 @@ import style from "./banner-css";
import classnames from "classnames"; import classnames from "classnames";
import tags from "../../../content/data/tags.json"; import tags from "../../../content/data/tags.json";
import fs from "fs"; import fs from "fs";
import { TagInfo } from "types/TagInfo";
const TAG_SVG_DEFAULT = fs.readFileSync("public/stickers/role_devops.svg", "utf-8"); const TAG_SVG_DEFAULT = fs.readFileSync("public/stickers/role_devops.svg", "utf-8");
const tagsMap = new Map(Object.entries(tags));
function BannerCodeScreen({ function BannerCodeScreen({
post, post,
@@ -19,8 +21,9 @@ function BannerCodeScreen({
const rotX = (post.description.length % 20) - 10; const rotX = (post.description.length % 20) - 10;
const rotY = (post.title.length * 3) % 20; const rotY = (post.title.length * 3) % 20;
const tagInfo = post.tags.map(tag => tags[tag]) const tagInfo = post.tags.map(tag => tagsMap.get(tag))
.filter(t => t?.emoji || (t?.image && t?.shownWithBranding))[0]; .filter((t): t is TagInfo => !!t)
.filter(t => t.emoji || (t.image && t.shownWithBranding))[0];
const tagSvg = tagInfo?.image const tagSvg = tagInfo?.image
? fs.readFileSync("public" + tagInfo.image, "utf-8") ? fs.readFileSync("public" + tagInfo.image, "utf-8")

View File

@@ -72,7 +72,7 @@ const TwitterLargeCard = ({
</div> </div>
<div class="postInfo"> <div class="postInfo">
<span class="authors"> <span class="authors">
{post.authors.map((id) => getUnicornById(id, post.locale).name).join(", ")} {post.authors.map((id) => getUnicornById(id, post.locale)!.name).join(", ")}
</span> </span>
<span class="date"> <span class="date">
{post.publishedMeta} &nbsp;&middot;&nbsp; {post.wordCount.toLocaleString("en")} words {post.publishedMeta} &nbsp;&middot;&nbsp; {post.wordCount.toLocaleString("en")} words

View File

@@ -13,7 +13,7 @@ export const layouts: Layout[] = [banner, twitterPreview];
const __dirname = dirname(fileURLToPath(import.meta.url)); const __dirname = dirname(fileURLToPath(import.meta.url));
const post = getPostBySlug("async-pipe-is-not-pure", "en"); const post = getPostBySlug("async-pipe-is-not-pure", "en")!;
const rebuild = async () => { const rebuild = async () => {
console.log("rebuilding..."); console.log("rebuilding...");

View File

@@ -1,6 +1,6 @@
import { PostInfo } from "types/index"; import { PostInfo } from "types/index";
import { render } from "preact-render-to-string"; import { render } from "preact-render-to-string";
import { createElement } from "preact"; import { VNode, createElement } from "preact";
import sharp from "sharp"; import sharp from "sharp";
import { unified } from "unified"; import { unified } from "unified";
import remarkParse from "remark-parse"; import remarkParse from "remark-parse";
@@ -68,7 +68,7 @@ export const renderPostPreviewToString = async (
const authorImageMap = Object.fromEntries( const authorImageMap = Object.fromEntries(
await Promise.all( await Promise.all(
post.authors.map(async (authorId) => { post.authors.map(async (authorId) => {
const author = getUnicornById(authorId, post.locale); const author = getUnicornById(authorId, post.locale)!;
if (authorImageCache.has(author.id)) if (authorImageCache.has(author.id))
return [author.id, authorImageCache.get(author.id)]; return [author.id, authorImageCache.get(author.id)];
@@ -113,7 +113,7 @@ export const renderPostPreviewToString = async (
width: PAGE_WIDTH, width: PAGE_WIDTH,
height: PAGE_HEIGHT, height: PAGE_HEIGHT,
authorImageMap, authorImageMap,
}), }) as VNode<{}>,
)} )}
</body> </body>
</html> </html>

20
package-lock.json generated
View File

@@ -34,6 +34,7 @@
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/json5": "^2.2.0", "@types/json5": "^2.2.0",
"@types/node": "^20.5.0", "@types/node": "^20.5.0",
"@types/probe-image-size": "^7.2.2",
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0", "@typescript-eslint/parser": "^6.3.0",
@@ -7273,6 +7274,15 @@
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz",
"integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==" "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA=="
}, },
"node_modules/@types/needle": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@types/needle/-/needle-3.2.2.tgz",
"integrity": "sha512-xUKAjFjDcucpgfyTvnbaqN+WBKyM9UehBuVRI/1AoPbaXrCScADqeTgTM1ZBYnS3Ovs9IEQt813IcJNyac7dNQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/nlcst": { "node_modules/@types/nlcst": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-1.0.1.tgz",
@@ -7309,6 +7319,16 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/@types/probe-image-size": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/@types/probe-image-size/-/probe-image-size-7.2.2.tgz",
"integrity": "sha512-rNWbJLj3XdCfiHsaDVTkPgvTtxlVMgNEpGpJYU/d2pVwzIpjumZguQzlnOQPWF7ajPOpX00rmPQGerRlB3tL+g==",
"dev": true,
"dependencies": {
"@types/needle": "*",
"@types/node": "*"
}
},
"node_modules/@types/resolve": { "node_modules/@types/resolve": {
"version": "1.20.2", "version": "1.20.2",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",

View File

@@ -58,6 +58,7 @@
"@types/jest": "^29.5.2", "@types/jest": "^29.5.2",
"@types/json5": "^2.2.0", "@types/json5": "^2.2.0",
"@types/node": "^20.5.0", "@types/node": "^20.5.0",
"@types/probe-image-size": "^7.2.2",
"@types/uuid": "^9.0.2", "@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^6.3.0", "@typescript-eslint/eslint-plugin": "^6.3.0",
"@typescript-eslint/parser": "^6.3.0", "@typescript-eslint/parser": "^6.3.0",

View File

@@ -15,7 +15,7 @@ import {
useRadioGroupState, useRadioGroupState,
} from "react-stately"; } from "react-stately";
const RadioContext = createContext<RadioGroupState>(null); const RadioContext = createContext<RadioGroupState | null>(null);
interface RadioButtonGroupProps extends PropsWithChildren<RadioGroupProps> { interface RadioButtonGroupProps extends PropsWithChildren<RadioGroupProps> {
class?: string; class?: string;
@@ -51,6 +51,10 @@ export function RadioButtonGroup(props: RadioButtonGroupProps) {
export function RadioButton(props: AriaRadioProps) { export function RadioButton(props: AriaRadioProps) {
const { children } = props; const { children } = props;
const state = useContext(RadioContext); const state = useContext(RadioContext);
if (!state) {
throw new Error("<RadioButton> must only be used within a <RadioButtonGroup>!");
}
const ref = useRef(null); const ref = useRef(null);
const { inputProps, isSelected } = useRadio(props, state, ref); const { inputProps, isSelected } = useRadio(props, state, ref);
const { isFocusVisible, focusProps } = useFocusRing(); const { isFocusVisible, focusProps } = useFocusRing();

View File

@@ -1,11 +1,19 @@
import { JSXNode, PropsWithChildren } from "../types"; import { JSXNode, PropsWithChildren } from "../types";
import { createElement, Ref, VNode } from "preact";
import { JSX } from "preact"; import { JSX } from "preact";
import { forwardRef } from "preact/compat"; import { ForwardedRef, forwardRef } from "preact/compat";
import { useMemo } from "preact/hooks";
type AllowedTags = "a" | "button" | "span" | "div"; type AllowedTags = "a" | "button" | "span" | "div";
type AllowedElements<Tag extends AllowedTags> = (
Tag extends "a"
? HTMLAnchorElement
: Tag extends "div"
? HTMLDivElement
: Tag extends "span"
? HTMLSpanElement
: HTMLButtonElement
);
type ButtonProps<Tag extends AllowedTags> = PropsWithChildren< type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
{ {
tag?: Tag; tag?: Tag;
@@ -19,19 +27,11 @@ type ButtonProps<Tag extends AllowedTags> = PropsWithChildren<
| "secondary-emphasized" | "secondary-emphasized"
| "primary" | "primary"
| "secondary"; | "secondary";
} & JSX.HTMLAttributes< } & JSX.HTMLAttributes<AllowedElements<Tag>>
Tag extends "a"
? HTMLAnchorElement
: Tag extends "div"
? HTMLDivElement
: Tag extends "span"
? HTMLSpanElement
: HTMLButtonElement
>
>; >;
const ButtonWrapper = forwardRef( const ButtonWrapper = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
<T extends AllowedTags = "a">( (
{ {
tag = "a" as never, tag = "a" as never,
class: className, class: className,
@@ -41,16 +41,8 @@ const ButtonWrapper = forwardRef(
rightIcon, rightIcon,
isFocusVisible, isFocusVisible,
...props ...props
}: ButtonProps<T>, },
ref: Ref< ref,
T extends "a"
? HTMLAnchorElement
: T extends "div"
? HTMLDivElement
: T extends "span"
? HTMLSpanElement
: HTMLButtonElement
>,
) => { ) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const Wrapper: any = tag; const Wrapper: any = tag;
@@ -85,10 +77,10 @@ const ButtonWrapper = forwardRef(
}, },
); );
export const Button = forwardRef( export const Button = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
<T extends AllowedTags = "a">( (
{ class: className = "", ...props }: ButtonProps<T>, { class: className = "", ...props },
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>, ref,
) => { ) => {
return ( return (
<ButtonWrapper <ButtonWrapper
@@ -100,10 +92,10 @@ export const Button = forwardRef(
}, },
); );
export const LargeButton = forwardRef( export const LargeButton = forwardRef<AllowedElements<AllowedTags> | null, ButtonProps<AllowedTags>>(
<T extends AllowedTags = "a">( (
{ class: className = "", ...props }: ButtonProps<T>, { class: className = "", ...props },
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>, ref,
) => { ) => {
return ( return (
<ButtonWrapper <ButtonWrapper
@@ -120,10 +112,10 @@ type IconOnlyButtonProps<T extends AllowedTags = "a"> = Omit<
"leftIcon" | "rightIcon" "leftIcon" | "rightIcon"
>; >;
export const IconOnlyButton = forwardRef( export const IconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
<T extends AllowedTags = "a">( (
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>, { class: className = "", children, ...props },
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>, ref,
) => { ) => {
return ( return (
<ButtonWrapper <ButtonWrapper
@@ -139,10 +131,10 @@ export const IconOnlyButton = forwardRef(
}, },
); );
export const LargeIconOnlyButton = forwardRef( export const LargeIconOnlyButton = forwardRef<AllowedElements<AllowedTags> | null, IconOnlyButtonProps<AllowedTags>>(
<T extends AllowedTags = "a">( (
{ class: className = "", children, ...props }: IconOnlyButtonProps<T>, { class: className = "", children, ...props },
ref: Ref<T extends "a" ? HTMLAnchorElement : HTMLButtonElement>, ref,
) => { ) => {
return ( return (
<ButtonWrapper {...props} class={`iconOnly large ${className}`} ref={ref}> <ButtonWrapper {...props} class={`iconOnly large ${className}`} ref={ref}>

View File

@@ -56,7 +56,7 @@ export const CollectionCard = ({
className={`text-style-button-regular ${style.authorListItem}`} className={`text-style-button-regular ${style.authorListItem}`}
> >
<UUPicture <UUPicture
picture={unicornProfilePicMap.find((u) => u.id === author.id)} picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
alt="" alt=""
class={style.authorImage} class={style.authorImage}
/> />

View File

@@ -22,7 +22,7 @@ export function Dialog(props: DialogProps) {
if (dialogRef.current) { if (dialogRef.current) {
if (props.open && !dialogRef.current.open) { if (props.open && !dialogRef.current.open) {
// reset the return value when re-opening the dialog // reset the return value when re-opening the dialog
dialogRef.current.returnValue = undefined; dialogRef.current.returnValue = "";
dialogRef.current.showModal(); dialogRef.current.showModal();
} }

View File

@@ -1,6 +1,6 @@
export const onSoftNavClick = export const onSoftNavClick =
(softNavigate: (href: string) => void) => (e: MouseEvent) => { (softNavigate: (href: string) => void) => (e: MouseEvent) => {
let link = e.target as HTMLElement; let link = e.target as HTMLElement | null;
// Could click on a child element of an anchor // Could click on a child element of an anchor
while (link && !(link instanceof HTMLAnchorElement)) { while (link && !(link instanceof HTMLAnchorElement)) {
link = link.parentElement; link = link.parentElement;

View File

@@ -21,7 +21,7 @@ import { DOMProps } from "@react-types/shared";
function PopupContents( function PopupContents(
props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & { props: Pick<PaginationProps, "page" | "getPageHref" | "softNavigate"> & {
titleId: string; titleId?: string;
close: () => void; close: () => void;
}, },
) { ) {
@@ -32,6 +32,8 @@ function PopupContents(
class={style.popupInner} class={style.popupInner}
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
if (!props.getPageHref) return;
if (props.softNavigate) { if (props.softNavigate) {
props.softNavigate(props.getPageHref(count)); props.softNavigate(props.getPageHref(count));
props.close(); props.close();

View File

@@ -23,7 +23,7 @@ export function PostCardGrid({
return ( return (
<ul {...props} class={style.list} role="list" id="post-list-container"> <ul {...props} class={style.list} role="list" id="post-list-container">
{postsToDisplay.map((post, i) => { {postsToDisplay.map((post, i) => {
const authors = post.authors.map(id => postAuthors.get(id)) const authors = post.authors.map(id => postAuthors.get(id)!)
.filter(u => !!u); .filter(u => !!u);
return expanded && post.bannerImg ? ( return expanded && post.bannerImg ? (

View File

@@ -213,8 +213,10 @@ function ListBox(props: ListBoxProps) {
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted // As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
// ...but nothing should be using these IDs anyway. // ...but nothing should be using these IDs anyway.
listBoxProps["id"] = undefined; Object.assign(listBoxProps, {
listBoxProps["aria-labelledby"] = undefined; ["id"]: undefined,
["aria-labelledby"]: undefined,
});
return ( return (
<ul {...listBoxProps} ref={listBoxRef} class={styles.optionsList}> <ul {...listBoxProps} ref={listBoxRef} class={styles.optionsList}>
@@ -242,8 +244,10 @@ export function Option({ item, state }: OptionProps) {
// As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted // As this is inside a portal (within <Popover>), nothing from Preact's useId can be trusted
// ...but nothing should be using these IDs anyway. // ...but nothing should be using these IDs anyway.
optionProps["aria-labelledby"] = undefined; Object.assign(optionProps, {
optionProps["aria-describedby"] = undefined; ["aria-labelledby"]: undefined,
["aria-describedby"]: undefined,
});
return ( return (
<li <li

View File

@@ -7,12 +7,13 @@ interface UseElementSizeProps {
export const useElementSize = ({ export const useElementSize = ({
includeMargin = true, includeMargin = true,
}: UseElementSizeProps = {}) => { }: UseElementSizeProps = {}) => {
const [el, setEl] = useState<HTMLElement>(null); const [el, setEl] = useState<HTMLElement|null>(null);
const [size, setSize] = useState({ width: 0, height: 0 }); const [size, setSize] = useState({ width: 0, height: 0 });
useLayoutEffect(() => { useLayoutEffect(() => {
if (!el) return; if (!el) return;
function getElementSize() { function getElementSize() {
if (!el) return;
const style = window.getComputedStyle(el); const style = window.getComputedStyle(el);
let calculatedHeight = el.offsetHeight; let calculatedHeight = el.offsetHeight;
let calculatedWidth = el.offsetWidth; let calculatedWidth = el.offsetWidth;

View File

@@ -32,8 +32,8 @@ export const get = () => {
.map((id) => getUnicornById(id, post.locale)) .map((id) => getUnicornById(id, post.locale))
.map((author) => { .map((author) => {
return { return {
name: author.name, name: author!.name,
link: `${siteUrl}/unicorns/${author.id}`, link: `${siteUrl}/unicorns/${author!.id}`,
}; };
}), }),
date: new Date(post.published), date: new Date(post.published),

View File

@@ -25,7 +25,7 @@ const userLogins = getUnicornsByLang("en")
.filter((unicorn) => !!unicorn.socials.github) .filter((unicorn) => !!unicorn.socials.github)
.map((unicorn) => unicorn.socials.github); .map((unicorn) => unicorn.socials.github);
const userResult: Record<string, { id: string }> = await octokit?.graphql(` const userResult = (await octokit?.graphql(`
query { query {
${userLogins.map( ${userLogins.map(
(login, i) => ` (login, i) => `
@@ -35,12 +35,12 @@ query {
`, `,
)} )}
} }
`); `)) as Record<string, { id: string }>;
const userIds: Record<string, string> = {}; const userIds: Record<string, string> = {};
if (userResult) { if (userResult) {
userLogins.forEach((login, i) => { userLogins.forEach((login, i) => {
userIds[login] = userResult[`user${i}`].id; if (login !== undefined) userIds[login] = userResult[`user${i}`].id;
}); });
} }

View File

@@ -80,44 +80,44 @@ export async function* getAchievements(
}; };
} }
if (data?.issueCount >= 25) { if (data && data.issueCount >= 25) {
yield { yield {
name: "Insect infestation!", name: "Insect infestation!",
body: `Open 25 issues in our GitHub repo`, body: `Open 25 issues in our GitHub repo`,
}; };
} else if (data?.issueCount >= 10) { } else if (data && data.issueCount >= 10) {
yield { yield {
name: "Creepy crawlies!", name: "Creepy crawlies!",
body: "Open 10 issues in our GitHub repo", body: "Open 10 issues in our GitHub repo",
}; };
} else if (data?.issueCount > 0) { } else if (data && data.issueCount > 0) {
yield { yield {
name: "Bug!", name: "Bug!",
body: "Open an issue in our GitHub repo", body: "Open an issue in our GitHub repo",
}; };
} }
if (data?.pullRequestCount >= 30) { if (data && data.pullRequestCount >= 30) {
yield { yield {
name: "Rabid Requester", name: "Rabid Requester",
body: `Open 30 pull requests in our GitHub repo`, body: `Open 30 pull requests in our GitHub repo`,
}; };
} else if (data?.pullRequestCount >= 10) { } else if (data && data.pullRequestCount >= 10) {
yield { yield {
name: "Request Rampage", name: "Request Rampage",
body: "Open 10 pull requests in our GitHub repo", body: "Open 10 pull requests in our GitHub repo",
}; };
} else if (data?.pullRequestCount >= 5) { } else if (data && data.pullRequestCount >= 5) {
yield { yield {
name: "Request Robot", name: "Request Robot",
body: "Open 5 pull requests in our GitHub repo", body: "Open 5 pull requests in our GitHub repo",
}; };
} else if (data?.pullRequestCount >= 3) { } else if (data && data.pullRequestCount >= 3) {
yield { yield {
name: "Request Racer", name: "Request Racer",
body: "Open 3 pull requests in our GitHub repo", body: "Open 3 pull requests in our GitHub repo",
}; };
} else if (data?.pullRequestCount > 0) { } else if (data && data.pullRequestCount > 0) {
yield { yield {
name: "Request Ranger", name: "Request Ranger",
body: "Open a pull request in our GitHub repo", body: "Open a pull request in our GitHub repo",
@@ -142,7 +142,7 @@ export async function* getAchievements(
} }
for (const year of contributorYears) { for (const year of contributorYears) {
if (data?.commitsInYear?.includes(year)) { if (data && data.commitsInYear?.includes(year)) {
yield { yield {
name: `${year} Contributor`, name: `${year} Contributor`,
body: `Make a commit to the site in ${year}!`, body: `Make a commit to the site in ${year}!`,

View File

@@ -18,6 +18,7 @@ export function getUnicornById(
language: Languages, language: Languages,
): UnicornInfo | undefined { ): UnicornInfo | undefined {
const locales = unicorns.get(id); const locales = unicorns.get(id);
if (!locales) return undefined;
return locales.find((u) => u.locale === language) || locales[0]; return locales.find((u) => u.locale === language) || locales[0];
} }
@@ -51,7 +52,9 @@ export function getPostsByCollection(
.map((locales) => locales.find((p) => p.locale === language) || locales[0]) .map((locales) => locales.find((p) => p.locale === language) || locales[0])
.filter((p) => p?.collection === collectionSlug) .filter((p) => p?.collection === collectionSlug)
.filter((p) => !p.noindex) .filter((p) => !p.noindex)
.sort((postA, postB) => (postA.order > postB.order ? 1 : -1)); .sort((postA, postB) =>
Number(postA.order) > Number(postB.order) ? 1 : -1,
);
} }
export function getPostsByUnicorn( export function getPostsByUnicorn(

View File

@@ -49,7 +49,7 @@ const tagExplainerParser = unified()
for (const [key, tag] of Object.entries(tagsRaw)) { for (const [key, tag] of Object.entries(tagsRaw)) {
let explainer = undefined; let explainer = undefined;
let explainerType = undefined; let explainerType: TagInfo["explainerType"] | undefined = undefined;
if ("image" in tag && tag.image.endsWith(".svg")) { if ("image" in tag && tag.image.endsWith(".svg")) {
const license = await fs const license = await fs
@@ -100,6 +100,9 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
const frontmatter = matter(fileContents).data as RawUnicornInfo; const frontmatter = matter(fileContents).data as RawUnicornInfo;
const profileImgSize = getImageSize(frontmatter.profileImg, unicornPath); const profileImgSize = getImageSize(frontmatter.profileImg, unicornPath);
if (!profileImgSize) {
throw new Error(`${unicornPath}: Unable to parse profile image size`);
}
const unicorn: UnicornInfo = { const unicorn: UnicornInfo = {
pronouns: "", pronouns: "",
@@ -115,7 +118,7 @@ async function readUnicorn(unicornPath: string): Promise<UnicornInfo[]> {
profileImgMeta: { profileImgMeta: {
height: profileImgSize.height as number, height: profileImgSize.height as number,
width: profileImgSize.width as number, width: profileImgSize.width as number,
...resolvePath(frontmatter.profileImg, unicornPath), ...resolvePath(frontmatter.profileImg, unicornPath)!,
}, },
}; };
@@ -178,14 +181,17 @@ async function readCollection(
const frontmatter = matter(fileContents).data as RawCollectionInfo; const frontmatter = matter(fileContents).data as RawCollectionInfo;
const coverImgSize = getImageSize(frontmatter.coverImg, collectionPath); const coverImgSize = getImageSize(frontmatter.coverImg, collectionPath);
if (!coverImgSize) {
throw new Error(`${collectionPath}: Unable to parse cover image size`);
}
const coverImgMeta = { const coverImgMeta = {
height: coverImgSize.height as number, height: coverImgSize.height as number,
width: coverImgSize.width as number, width: coverImgSize.width as number,
...resolvePath(frontmatter.coverImg, collectionPath), ...resolvePath(frontmatter.coverImg, collectionPath)!,
}; };
const frontmatterTags = [...frontmatter.tags].filter((tag) => { const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
if (tags.has(tag)) { if (tags.has(tag)) {
return true; return true;
} else { } else {
@@ -267,7 +273,7 @@ async function readPost(
// get an excerpt of the post markdown no longer than 150 chars // get an excerpt of the post markdown no longer than 150 chars
const excerpt = getExcerpt(fileMatter.content, 150); const excerpt = getExcerpt(fileMatter.content, 150);
const frontmatterTags = [...frontmatter.tags].filter((tag) => { const frontmatterTags = (frontmatter.tags || []).filter((tag) => {
if (tags.has(tag)) { if (tags.has(tag)) {
return true; return true;
} else { } else {

View File

@@ -17,7 +17,9 @@
const sizeStringRegex = / ([0-9.]+)([xw])$/; const sizeStringRegex = / ([0-9.]+)([xw])$/;
const parseSourceString = (source: string) => { const parseSourceString = (source: string) => {
const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(source); const [fullMatch, sizeStr, sizeType] = sizeStringRegex.exec(
source,
) as string[];
const srcStr = source.replace(fullMatch, ""); const srcStr = source.replace(fullMatch, "");
return { return {
src: srcStr, src: srcStr,

View File

@@ -46,17 +46,17 @@ const getOrderRange = (arr: PostInfo[]) => {
smallest: curr, smallest: curr,
}; };
} }
if (curr.order! < prev.smallest.order!) { if (curr.order! < prev.smallest!.order!) {
prev.smallest = curr; prev.smallest = curr;
} }
if (curr.order! > prev.largest.order!) { if (curr.order! > prev.largest!.order!) {
prev.largest = curr; prev.largest = curr;
} }
return prev; return prev;
}, },
{ {
largest: null as PostInfo, largest: undefined as PostInfo | undefined,
smallest: null as PostInfo, smallest: undefined as PostInfo | undefined,
}, },
); );
}; };
@@ -104,8 +104,8 @@ export const getSuggestedArticles = (postNode: PostInfo) => {
const { largest, smallest } = getOrderRange(suggestedArticles) || {}; const { largest, smallest } = getOrderRange(suggestedArticles) || {};
for (const suggestedPost of extraSuggestedArticles) { for (const suggestedPost of extraSuggestedArticles) {
if ( if (
suggestedPost.order === smallest.order! - 1 || suggestedPost.order === smallest!.order! - 1 ||
suggestedPost.order === largest.order! + 1 suggestedPost.order === largest!.order! + 1
) { ) {
suggestedArticles.push(suggestedPost); suggestedArticles.push(suggestedPost);
} }

View File

@@ -30,11 +30,19 @@ function isNestedElement(e: MouseEvent) {
if (target.getAttribute("data-dont-bind-navigate-click") !== null) if (target.getAttribute("data-dont-bind-navigate-click") !== null)
return true; return true;
target = target.parentElement; if (target.parentElement) target = target.parentElement;
else break;
} }
return false; return false;
} }
declare global {
function handleHrefContainerMouseDown(e: MouseEvent): void;
function handleHrefContainerMouseUp(e: MouseEvent): void;
function handleHrefContainerAuxClick(e: MouseEvent): void;
function handleHrefContainerClick(e: MouseEvent): void;
}
globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => { globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
const isMiddleClick = e.button === 1; const isMiddleClick = e.button === 1;
if (!isMiddleClick) return; if (!isMiddleClick) return;
@@ -46,7 +54,7 @@ globalThis.handleHrefContainerMouseDown = (e: MouseEvent) => {
// implement the AuxClick event using MouseUp (only on browsers that don't support auxclick; i.e. safari) // implement the AuxClick event using MouseUp (only on browsers that don't support auxclick; i.e. safari)
globalThis.handleHrefContainerMouseUp = (e: MouseEvent) => { globalThis.handleHrefContainerMouseUp = (e: MouseEvent) => {
// if auxclick is supported, do nothing // if auxclick is supported, do nothing
if ("onauxclick" in e.currentTarget) return; if (e.currentTarget && "onauxclick" in e.currentTarget) return;
// otherwise, pass mouseup events to auxclick // otherwise, pass mouseup events to auxclick
globalThis.handleHrefContainerAuxClick(e); globalThis.handleHrefContainerAuxClick(e);
}; };
@@ -95,7 +103,7 @@ globalThis.handleHrefContainerClick = (e: MouseEvent) => {
) )
return; return;
window.location.href = href; window.location.href = String(href);
}; };
export function getHrefContainerProps(href: string) { export function getHrefContainerProps(href: string) {

View File

@@ -20,12 +20,12 @@
*/ */
import { toString } from "hast-util-to-string"; import { toString } from "hast-util-to-string";
import type { Child as HChild } from "hastscript"; import type { Child as HChild } from "hastscript";
import { Element } from "hast"; import { Data, Element, Parent, Node } from "hast";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import replaceAllBetween from "unist-util-replace-all-between"; import replaceAllBetween from "unist-util-replace-all-between";
import { Node } from "unist";
import JSON5 from "json5"; import JSON5 from "json5";
import { FileList, Directory, File } from "./file-list"; import { FileList, Directory, File } from "./file-list";
import { Root } from "postcss";
interface DirectoryMetadata { interface DirectoryMetadata {
open?: boolean; open?: boolean;
@@ -34,19 +34,24 @@ interface DirectoryMetadata {
interface FileMetadata {} interface FileMetadata {}
export const rehypeFileTree = () => { export const rehypeFileTree = () => {
return (tree) => { return (tree: Root) => {
function replaceFiletreeNodes(nodes: Node[]) { function replaceFiletreeNodes(nodes: Node[]) {
const items: Array<Directory | File> = []; const items: Array<Directory | File> = [];
const isNodeElement = (node: unknown): node is Element => const isNodeElement = (node: unknown): node is Element =>
typeof node === "object" && node["type"] === "element"; (typeof node === "object" &&
node &&
"type" in node &&
node["type"] === "element") ??
false;
function traverseUl(listNode: Element, listItems: typeof items) { function traverseUl(listNode: Element, listItems: typeof items) {
if (listNode.children.length === 0) return; if (listNode.children.length === 0) return;
for (const listItem of listNode.children) { for (const listItem of listNode.children) {
// Filter out `\n` text nodes // Filter out `\n` text nodes
if (!(isNodeElement(listItem) && listItem.tagName === "li")) continue; if (!(listItem.type === "element" && listItem.tagName === "li"))
continue;
// Strip nodes that only contain newlines // Strip nodes that only contain newlines
listItem.children = listItem.children.filter( listItem.children = listItem.children.filter(
@@ -165,13 +170,13 @@ export const rehypeFileTree = () => {
} }
replaceAllBetween( replaceAllBetween(
tree, tree as never as Parent,
{ type: "raw", value: "<!-- filetree:start -->" } as never, { type: "raw", value: "<!-- filetree:start -->" } as never,
{ type: "raw", value: "<!-- filetree:end -->" } as never, { type: "raw", value: "<!-- filetree:end -->" } as never,
replaceFiletreeNodes, replaceFiletreeNodes,
); );
replaceAllBetween( replaceAllBetween(
tree, tree as never as Parent,
{ type: "comment", value: " filetree:start " } as never, { type: "comment", value: " filetree:start " } as never,
{ type: "comment", value: " filetree:end " } as never, { type: "comment", value: " filetree:end " } as never,
replaceFiletreeNodes, replaceFiletreeNodes,

View File

@@ -19,15 +19,17 @@ function isNodeSummary(e: Node) {
*/ */
export const rehypeHints: Plugin<[], Root> = () => { export const rehypeHints: Plugin<[], Root> = () => {
return (tree) => { return (tree) => {
visit(tree, (node: Element, index, parent: Element) => { visit(tree, "element", (node: Element, index, parent) => {
if (node.tagName !== "details") return; if (node.tagName !== "details") return;
const summaryNode = node.children.find(isNodeSummary); const summaryNode = node.children.find(isNodeSummary);
if (index !== undefined && parent?.children) {
parent.children[index] = Hint({ parent.children[index] = Hint({
title: toString(summaryNode as never), title: toString(summaryNode as never),
children: node.children.filter((e) => !isNodeSummary(e)), children: node.children.filter((e) => !isNodeSummary(e)),
}); });
}
}); });
}; };
}; };

View File

@@ -9,12 +9,10 @@ export const iFrameClickToRun = () => {
[...iframeButtons].forEach((el) => { [...iframeButtons].forEach((el) => {
el.addEventListener("click", () => { el.addEventListener("click", () => {
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
// eslint-disable-next-line @typescript-eslint/no-explicit-any const parent = el.parentElement!;
(iframe as any).loading = "lazy"; iframe.loading = "lazy";
iframe.src = el.parentElement.dataset.iframeurl; iframe.src = String(parent.dataset.iframeurl);
const propsToPreserve = JSON.parse( const propsToPreserve = JSON.parse(parent.dataset.iframeprops || "{}");
el.parentElement.dataset.iframeprops || "{}",
);
for (const prop in propsToPreserve) { for (const prop in propsToPreserve) {
const val = propsToPreserve[prop]; const val = propsToPreserve[prop];
// Handle array props per hast spec: // Handle array props per hast spec:
@@ -25,9 +23,9 @@ export const iFrameClickToRun = () => {
} }
iframe.setAttribute(prop, propsToPreserve[prop]); iframe.setAttribute(prop, propsToPreserve[prop]);
} }
iframe.style.width = el.parentElement.style.width; iframe.style.width = parent.style.width;
iframe.style.height = el.parentElement.style.height; iframe.style.height = parent.style.height;
el.parentElement.replaceWith(iframe); parent.replaceWith(iframe);
}); });
}); });
}; };

View File

@@ -1,5 +1,4 @@
import { Root, Element } from "hast"; import { Root, Element } from "hast";
import { Plugin } from "unified";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
@@ -13,8 +12,6 @@ import type { GetPictureResult } from "@astrojs/image/dist/lib/get-picture";
import probe from "probe-image-size"; import probe from "probe-image-size";
import { IFramePlaceholder } from "./iframe-placeholder"; import { IFramePlaceholder } from "./iframe-placeholder";
interface RehypeUnicornIFrameClickToRunProps {}
// default icon, used if a frame's favicon cannot be resolved // default icon, used if a frame's favicon cannot be resolved
let defaultPageIcon: Promise<GetPictureResult>; let defaultPageIcon: Promise<GetPictureResult>;
function fetchDefaultPageIcon(): Promise<GetPictureResult> { function fetchDefaultPageIcon(): Promise<GetPictureResult> {
@@ -34,20 +31,21 @@ function fetchDefaultPageIcon(): Promise<GetPictureResult> {
// and multiple fetchPageInfo() calls can await the same icon // and multiple fetchPageInfo() calls can await the same icon
const pageIconMap = new Map<string, Promise<GetPictureResult>>(); const pageIconMap = new Map<string, Promise<GetPictureResult>>();
function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> { function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin); if (pageIconMap.has(src.origin)) return pageIconMap.get(src.origin)!;
const promise = (async () => { const promise = (async () => {
// <link rel="manifest" href="/manifest.json"> // <link rel="manifest" href="/manifest.json">
const manifestPath: Element = find( const manifestPath: Element | undefined = find(
srcHast, srcHast,
(node: unknown) => (node as Element)?.properties?.rel?.[0] === "manifest", (node: unknown) =>
(node as Element)?.properties?.rel?.toString() === "manifest",
); );
let iconLink: string; let iconLink: string | undefined;
if (manifestPath) { if (manifestPath?.properties?.href) {
// `/manifest.json` // `/manifest.json`
const manifestRelativeURL = manifestPath.properties.href.toString(); const manifestRelativeURL = String(manifestPath.properties.href);
const fullManifestURL = new URL(manifestRelativeURL, src).href; const fullManifestURL = new URL(manifestRelativeURL, src).href;
const manifest = await fetch(fullManifestURL) const manifest = await fetch(fullManifestURL)
@@ -56,6 +54,7 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
if (manifest) { if (manifest) {
const largestIcon = getLargestManifestIcon(manifest); const largestIcon = getLargestManifestIcon(manifest);
if (largestIcon?.icon)
iconLink = new URL(largestIcon.icon.src, src.origin).href; iconLink = new URL(largestIcon.icon.src, src.origin).href;
} }
} }
@@ -63,13 +62,14 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
if (!iconLink) { if (!iconLink) {
// fetch `favicon.ico` // fetch `favicon.ico`
// <link rel="shortcut icon" type="image/png" href="https://example.com/img.png"> // <link rel="shortcut icon" type="image/png" href="https://example.com/img.png">
const favicon: Element = find( const favicon: Element | undefined = find(
srcHast, srcHast,
(node: unknown) => (node: unknown) =>
(node as Element)?.properties?.rel?.toString()?.includes("icon"), (node as Element)?.properties?.rel?.toString()?.includes("icon") ??
false,
); );
if (favicon) { if (favicon?.properties?.href) {
iconLink = new URL(favicon.properties.href.toString(), src).href; iconLink = new URL(favicon.properties.href.toString(), src).href;
} }
} }
@@ -96,11 +96,11 @@ function fetchPageIcon(src: URL, srcHast: Root): Promise<GetPictureResult> {
const pageHtmlMap = new Map<string, Promise<Root | null>>(); const pageHtmlMap = new Map<string, Promise<Root | null>>();
function fetchPageHtml(src: string): Promise<Root | null> { function fetchPageHtml(src: string): Promise<Root | null> {
if (pageHtmlMap.has(src)) return pageHtmlMap.get(src); if (pageHtmlMap.has(src)) return pageHtmlMap.get(src)!;
const promise = (async () => { const promise = (async () => {
const srcHTML = await fetch(src) const srcHTML = await fetch(src)
.then((r) => r.status === 200 && r.text()) .then((r) => (r.status === 200 ? r.text() : undefined))
.catch(() => null); .catch(() => null);
// if fetch fails... // if fetch fails...
@@ -129,7 +129,7 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
if (!srcHast) return null; if (!srcHast) return null;
// find <title> element in response HTML // find <title> element in response HTML
const titleEl: Element = find(srcHast, { tagName: "title" }); const titleEl = find<Element>(srcHast, { tagName: "title" });
const titleContentEl = titleEl && titleEl.children[0]; const titleContentEl = titleEl && titleEl.children[0];
const title = const title =
titleContentEl?.type === "text" ? titleContentEl.value : undefined; titleContentEl?.type === "text" ? titleContentEl.value : undefined;
@@ -140,13 +140,10 @@ async function fetchPageInfo(src: string): Promise<PageInfo | null> {
} }
// TODO: Add switch/case and dedicated files ala "Components" // TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornIFrameClickToRun: Plugin< export const rehypeUnicornIFrameClickToRun = () => {
[RehypeUnicornIFrameClickToRunProps | never], return async (tree: Root) => {
Root
> = () => {
return async (tree) => {
const iframeNodes: Element[] = []; const iframeNodes: Element[] = [];
visit(tree, (node: Element) => { visit(tree, "element", (node: Element) => {
if (node.tagName === "iframe") { if (node.tagName === "iframe") {
iframeNodes.push(node); iframeNodes.push(node);
} }
@@ -168,7 +165,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
width = width ?? EMBED_SIZE.w; width = width ?? EMBED_SIZE.w;
height = height ?? EMBED_SIZE.h; height = height ?? EMBED_SIZE.h;
const info: PageInfo = (await fetchPageInfo( const info: PageInfo = (await fetchPageInfo(
iframeNode.properties.src.toString(), String(iframeNode.properties.src),
).catch(() => null)) || { icon: await fetchDefaultPageIcon() }; ).catch(() => null)) || { icon: await fetchDefaultPageIcon() };
const [, heightPx] = /^([0-9]+)(px)?$/.exec(height + "") || []; const [, heightPx] = /^([0-9]+)(px)?$/.exec(height + "") || [];
@@ -177,7 +174,7 @@ export const rehypeUnicornIFrameClickToRun: Plugin<
const iframeReplacement = IFramePlaceholder({ const iframeReplacement = IFramePlaceholder({
width: width.toString(), width: width.toString(),
height: height.toString(), height: height.toString(),
src: src.toString(), src: String(src),
pageTitle: String(dataFrameTitle ?? "") || info.title || "", pageTitle: String(dataFrameTitle ?? "") || info.title || "",
pageIcon: info.icon, pageIcon: info.icon,
propsToPreserve: JSON.stringify(propsToPreserve), propsToPreserve: JSON.stringify(propsToPreserve),

View File

@@ -1,4 +1,3 @@
import { Plugin } from "unified";
import rehypeSlug from "rehype-slug-custom-id"; import rehypeSlug from "rehype-slug-custom-id";
import rehypeRaw from "rehype-raw"; import rehypeRaw from "rehype-raw";
import { rehypeTabs } from "./tabs/rehype-transform"; import { rehypeTabs } from "./tabs/rehype-transform";
@@ -17,11 +16,17 @@ import { rehypeHeaderText } from "./rehype-header-text";
import { rehypeHeaderClass } from "./rehype-header-class"; import { rehypeHeaderClass } from "./rehype-header-class";
import { rehypeFileTree } from "./file-tree/rehype-file-tree"; import { rehypeFileTree } from "./file-tree/rehype-file-tree";
import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform"; import { rehypeTwoslashTabindex } from "./twoslash-tabindex/rehype-transform";
import { Plugin } from "unified";
// TODO: this does not actually validate that the adjacent config matches the type of the plugin in an array structure
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
type RehypePlugin = Plugin<any[]> | [Plugin<any[]>, any]; type RehypePlugin<T = any> =
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| (T | ((config: T) => any))[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
| (() => any);
export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] { export function createRehypePlugins(config: MarkdownConfig): Plugin[] {
return [ return [
// This is required to handle unsafe HTML embedded into Markdown // This is required to handle unsafe HTML embedded into Markdown
[rehypeRaw, { passThrough: [`mdxjsEsm`] }], [rehypeRaw, { passThrough: [`mdxjsEsm`] }],
@@ -29,7 +34,7 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
...(config.format === "epub" ...(config.format === "epub"
? [ ? [
rehypeFixTwoSlashXHTML, rehypeFixTwoSlashXHTML,
[rehypeMakeImagePathsAbsolute, { path: config.path }] as RehypePlugin, [rehypeMakeImagePathsAbsolute, { path: config.path }],
rehypeMakeHrefPathsAbsolute, rehypeMakeHrefPathsAbsolute,
] ]
: []), : []),
@@ -70,8 +75,8 @@ export function createRehypePlugins(config: MarkdownConfig): RehypePlugin[] {
className: (depth: number) => className: (depth: number) =>
`text-style-headline-${Math.min(depth + 1, 6)}`, `text-style-headline-${Math.min(depth + 1, 6)}`,
}, },
] as RehypePlugin, ],
] ]
: []), : []),
]; ] as RehypePlugin[] as Plugin[];
} }

View File

@@ -11,11 +11,9 @@ import path from "path";
*/ */
import { getPicture } from "./get-picture-hack"; import { getPicture } from "./get-picture-hack";
import { getImageSize } from "../get-image-size"; import { getImageSize } from "../get-image-size";
import { fileURLToPath } from "url";
import { resolvePath } from "../url-paths"; import { resolvePath } from "../url-paths";
import { getLargestSourceSetSrc } from "../get-largest-source-set-src"; import { getLargestSourceSetSrc } from "../get-largest-source-set-src";
import { ISizeCalculationResult } from "image-size/dist/types/interface";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const MAX_WIDTH = 768; const MAX_WIDTH = 768;
const MAX_HEIGHT = 768; const MAX_HEIGHT = 768;
@@ -33,7 +31,7 @@ function getPixelValue(attr: unknown): number | undefined {
export const rehypeAstroImageMd: Plugin<[], Root> = () => { export const rehypeAstroImageMd: Plugin<[], Root> = () => {
return async (tree, file) => { return async (tree, file) => {
const imgNodes: Element[] = []; const imgNodes: Element[] = [];
visit(tree, (node: Element) => { visit(tree, "element", (node: Element) => {
if (node.tagName === "img") { if (node.tagName === "img") {
imgNodes.push(node); imgNodes.push(node);
} }
@@ -72,7 +70,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
const nodeWidth = getPixelValue(node.properties.width); const nodeWidth = getPixelValue(node.properties.width);
const nodeHeight = getPixelValue(node.properties.height); const nodeHeight = getPixelValue(node.properties.height);
const dimensions = { ...srcSize }; const dimensions = { ...srcSize } as { width: number; height: number };
if (nodeHeight) { if (nodeHeight) {
dimensions.height = nodeHeight; dimensions.height = nodeHeight;
dimensions.width = Math.floor(nodeHeight * imageRatio); dimensions.width = Math.floor(nodeHeight * imageRatio);
@@ -119,7 +117,7 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
alt: nodeAlt || "", alt: nodeAlt || "",
}); });
pngSource = originalPictureResult.sources.reduce( const newPngSource = originalPictureResult.sources.reduce(
(prev, source) => { (prev, source) => {
const largestSrc = getLargestSourceSetSrc(source.srcset); const largestSrc = getLargestSourceSetSrc(source.srcset);
// select first option // select first option
@@ -146,8 +144,10 @@ export const rehypeAstroImageMd: Plugin<[], Root> = () => {
} }
return prev; return prev;
}, },
null as ReturnType<typeof getLargestSourceSetSrc>, undefined as ReturnType<typeof getLargestSourceSetSrc> | undefined,
); );
if (newPngSource) pngSource = newPngSource;
} }
const sources = pictureResult.sources.map((attrs) => { const sources = pictureResult.sources.map((attrs) => {

View File

@@ -3,6 +3,7 @@ import { hasProperty } from "hast-util-has-property";
import { toString } from "hast-util-to-string"; import { toString } from "hast-util-to-string";
import { Root, Parent } from "hast"; import { Root, Parent } from "hast";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import { AstroVFile } from "./types";
interface RehypeHeaderClassOpts { interface RehypeHeaderClassOpts {
depth: number; depth: number;
@@ -14,19 +15,22 @@ interface RehypeHeaderClassOpts {
* at the intended visual level. * at the intended visual level.
*/ */
export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => { export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
return (tree: Root, file) => { return (tree: Root, file: AstroVFile) => {
// hacky (temporary) fix to exclude the site/about-us*.mdx files, since // hacky (temporary) fix to exclude the site/about-us*.mdx files, since
// those start at a different heading level // those start at a different heading level
if (file.data.astro.frontmatter.slug === "site") return; if (file.data.astro.frontmatter.slug === "site") return;
// Find the minimum heading rank in the file // Find the minimum heading rank in the file
// (e.g. if it starts at h2, minDepth = 2) // (e.g. if it starts at h2, minDepth = 2)
let minDepth: number; let minDepth: number | undefined;
visit(tree, "element", (node: Parent["children"][number]) => { visit(tree, "element", (node: Parent["children"][number]) => {
const nodeHeadingRank = headingRank(node); const nodeHeadingRank = headingRank(node);
if (!minDepth || nodeHeadingRank < minDepth) minDepth = nodeHeadingRank; if (
!minDepth ||
(nodeHeadingRank !== undefined && nodeHeadingRank < minDepth)
)
minDepth = nodeHeadingRank;
}); });
minDepth ||= 1;
visit(tree, "element", (node: Parent["children"][number]) => { visit(tree, "element", (node: Parent["children"][number]) => {
const nodeHeadingRank = headingRank(node); const nodeHeadingRank = headingRank(node);
@@ -42,7 +46,7 @@ export const rehypeHeaderClass = (opts: RehypeHeaderClassOpts) => {
// - when (minDepth = 5, depth = 2) h5 + 2 - 4 -> h3 // - when (minDepth = 5, depth = 2) h5 + 2 - 4 -> h3
// - when (minDepth = 1, depth = 2) h1 + 2 + 0 -> h3 // - when (minDepth = 1, depth = 2) h1 + 2 + 0 -> h3
const tagHeadingRank = Math.min( const tagHeadingRank = Math.min(
nodeHeadingRank + opts.depth + (1 - minDepth), nodeHeadingRank + opts.depth + (1 - (minDepth ?? 1)),
6, 6,
); );
const className = opts.className(nodeHeadingRank); const className = opts.className(nodeHeadingRank);

View File

@@ -4,12 +4,13 @@ import { toString } from "hast-util-to-string";
import { Root, Parent } from "hast"; import { Root, Parent } from "hast";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import { PostHeadingInfo } from "src/types/index"; import { PostHeadingInfo } from "src/types/index";
import { AstroVFile } from "./types";
/** /**
* Plugin to add `data-header-text`s to headings. * Plugin to add `data-header-text`s to headings.
*/ */
export const rehypeHeaderText = () => { export const rehypeHeaderText = () => {
return (tree: Root, file) => { return (tree: Root, file: AstroVFile) => {
const headingsWithId: PostHeadingInfo[] = const headingsWithId: PostHeadingInfo[] =
(file.data.astro.frontmatter.headingsWithId = []); (file.data.astro.frontmatter.headingsWithId = []);

View File

@@ -1,14 +1,14 @@
import { Root, Element } from "hast"; import { Root, Element } from "hast";
import { Plugin } from "unified";
import { visit } from "unist-util-visit"; import { visit } from "unist-util-visit";
import { urlPathRegex, resolvePath } from "../url-paths"; import { urlPathRegex, resolvePath } from "../url-paths";
import path from "path"; import path from "path";
import { AstroVFile } from "./types";
// TODO: Add switch/case and dedicated files ala "Components" // TODO: Add switch/case and dedicated files ala "Components"
export const rehypeUnicornElementMap: Plugin<[], Root> = () => { export const rehypeUnicornElementMap = () => {
return async (tree, file) => { return async (tree: Root, file: AstroVFile) => {
visit(tree, (node: Element) => { visit(tree, "element", (node: Element) => {
if (node.tagName === "video") { if (node.tagName === "video") {
node.properties.muted ??= true; node.properties.muted ??= true;
node.properties.autoPlay ??= true; node.properties.autoPlay ??= true;
@@ -19,7 +19,7 @@ export const rehypeUnicornElementMap: Plugin<[], Root> = () => {
if (file.path) { if (file.path) {
const resolvedPath = resolvePath( const resolvedPath = resolvePath(
node.properties.src.toString(), String(node.properties.src),
path.dirname(file.path), path.dirname(file.path),
); );
if (resolvedPath) if (resolvedPath)

View File

@@ -8,7 +8,7 @@ export const TwitchTransformer = {
}, },
getHTML(url: string) { getHTML(url: string) {
const srcUrl = new URL(url); const srcUrl = new URL(url);
let embedUrl: URL; let embedUrl: URL | undefined = undefined;
if (srcUrl.host === "clips.twitch.tv") { if (srcUrl.host === "clips.twitch.tv") {
const clipId = const clipId =

View File

@@ -1,6 +1,5 @@
import { Root } from "hast"; import { Root } from "hast";
import replaceAllBetween from "unist-util-replace-all-between"; import replaceAllBetween from "unist-util-replace-all-between";
import { Plugin } from "unified";
import { getHeaderNodeId, slugs } from "rehype-slug-custom-id"; import { getHeaderNodeId, slugs } from "rehype-slug-custom-id";
import { Element, Node, Parent, Text } from "hast"; import { Element, Node, Parent, Text } from "hast";
import { TabInfo, Tabs } from "./tabs"; import { TabInfo, Tabs } from "./tabs";
@@ -77,8 +76,8 @@ const getApproxLineCount = (nodes: Node[], inParagraph?: boolean): number => {
* To align with React Tabs package: * To align with React Tabs package:
* @see https://github.com/reactjs/react-tabs * @see https://github.com/reactjs/react-tabs
*/ */
export const rehypeTabs: Plugin<[], Root> = () => { export const rehypeTabs = () => {
return (tree) => { return (tree: Root) => {
const replaceTabNodes = (nodes: Node[]) => { const replaceTabNodes = (nodes: Node[]) => {
let sectionStarted = false; let sectionStarted = false;
const largestSize = findLargestHeading(nodes as Element[]); const largestSize = findLargestHeading(nodes as Element[]);
@@ -109,18 +108,18 @@ export const rehypeTabs: Plugin<[], Root> = () => {
} }
// For any other heading found in the tab contents, append to the nested headers array // For any other heading found in the tab contents, append to the nested headers array
if (isNodeHeading(localNode)) { if (isNodeHeading(localNode) && tabs.length) {
const lastTab = tabs.at(-1); const lastTab = tabs.at(-1);
// Store the related tab ID in the attributes of the header // Store the related tab ID in the attributes of the header
localNode.properties["data-tabname"] = lastTab.slug; localNode.properties["data-tabname"] = lastTab?.slug;
// Add header ID to array // Add header ID to array
tabs.at(-1).headers.push(localNode.properties.id.toString()); tabs.at(-1)?.headers?.push(String(localNode.properties.id));
} }
// Otherwise, append the node as tab content // Otherwise, append the node as tab content
tabs.at(-1).contents.push(localNode); tabs.at(-1)?.contents?.push(localNode);
} }
// Determine if the set of tabs should use a constant height (via the "tabs-small" class) // Determine if the set of tabs should use a constant height (via the "tabs-small" class)

View File

@@ -60,6 +60,8 @@ export const enableTabs = () => {
function handleClick(e: Event) { function handleClick(e: Event) {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
const tabName = target.dataset.tabname; const tabName = target.dataset.tabname;
if (!tabName) return;
changeTabs(tabName); changeTabs(tabName);
if (shouldScrollToTab) { if (shouldScrollToTab) {
@@ -76,9 +78,11 @@ export const enableTabs = () => {
} }
// Iterate through all tabs to populate tabEntries & set listeners // Iterate through all tabs to populate tabEntries & set listeners
document.querySelectorAll('[role="tablist"]').forEach((tabList) => { document
.querySelectorAll<HTMLElement>('[role="tablist"]')
.forEach((tabList) => {
const entry: TabEntry = new Map(); const entry: TabEntry = new Map();
const parent = tabList.parentElement; const parent = tabList.parentElement!;
const tabs: NodeListOf<HTMLElement> = const tabs: NodeListOf<HTMLElement> =
tabList.querySelectorAll('[role="tab"]'); tabList.querySelectorAll('[role="tab"]');
@@ -86,8 +90,8 @@ export const enableTabs = () => {
tabs.forEach((tab) => { tabs.forEach((tab) => {
const panel = parent.querySelector<HTMLElement>( const panel = parent.querySelector<HTMLElement>(
`#${tab.getAttribute("aria-controls")}`, `#${tab.getAttribute("aria-controls")}`,
); )!;
entry.set(tab.dataset.tabname, { entry.set(tab.dataset.tabname!, {
tab, tab,
panel, panel,
}); });

View File

@@ -20,7 +20,7 @@ import { toString } from "hast-util-to-string";
*/ */
export const rehypeTooltips: Plugin<[], Root> = () => { export const rehypeTooltips: Plugin<[], Root> = () => {
return (tree) => { return (tree) => {
visit(tree, (node: Element, index, parent: Element) => { visit(tree, "element", (node: Element, index, parent) => {
if (node.tagName !== "blockquote") return; if (node.tagName !== "blockquote") return;
const firstParagraph = node.children.find((e) => e.type === "element"); const firstParagraph = node.children.find((e) => e.type === "element");
@@ -42,11 +42,13 @@ export const rehypeTooltips: Plugin<[], Root> = () => {
// remove `firstText` from children nodes // remove `firstText` from children nodes
firstParagraph.children.splice(0, 1); firstParagraph.children.splice(0, 1);
if (parent?.children && index !== undefined) {
parent.children[index] = Tooltip({ parent.children[index] = Tooltip({
icon: firstText.tagName === "em" ? "warning" : "info", icon: firstText.tagName === "em" ? "warning" : "info",
title: toString(firstText as never).replace(/:$/, ""), title: toString(firstText as never).replace(/:$/, ""),
children: node.children, children: node.children,
}); });
}
}); });
}; };
}; };

View File

@@ -4,7 +4,7 @@ import { visit } from "unist-util-visit";
export const rehypeTwoslashTabindex: Plugin<[], Root> = () => { export const rehypeTwoslashTabindex: Plugin<[], Root> = () => {
return async (tree, _) => { return async (tree, _) => {
visit(tree, (node: Element) => { visit(tree, "element", (node: Element) => {
if ( if (
node.tagName === "div" && node.tagName === "div" &&
node.properties.className instanceof Array && node.properties.className instanceof Array &&

View File

@@ -3,8 +3,8 @@ import { languages } from "../constants/index";
import { basename } from "path"; import { basename } from "path";
import { MDXInstance, MarkdownInstance } from "astro"; import { MDXInstance, MarkdownInstance } from "astro";
function isLanguageKey(str: string): str is Languages { function isLanguageKey(str: string | undefined): str is Languages {
return Object.keys(languages).includes(str); return str !== undefined && Object.keys(languages).includes(str);
} }
/** /**
@@ -116,7 +116,7 @@ export function getTranslatedPage<
} }
// fetch translation files from /data/i18n // fetch translation files from /data/i18n
let i18nFiles: Record<string, unknown>; let i18nFiles: Record<string, { default: Record<string, string> }>;
try { try {
i18nFiles = import.meta.glob("../../content/data/i18n/*.json", { i18nFiles = import.meta.glob("../../content/data/i18n/*.json", {
eager: true, eager: true,
@@ -127,12 +127,10 @@ try {
const i18n: Partial<Record<Languages, Map<string, string>>> = const i18n: Partial<Record<Languages, Map<string, string>>> =
Object.fromEntries( Object.fromEntries(
Object.entries(i18nFiles).map( Object.entries(i18nFiles).map(([file, content]) => [
([file, content]: [string, { default: Record<string, string> }]) => [
basename(file).split(".")[0], basename(file).split(".")[0],
new Map(Object.entries(content.default)), new Map(Object.entries(content.default)),
], ]),
),
); );
// warn about any values that do not have full translations // warn about any values that do not have full translations

View File

@@ -8,7 +8,7 @@ import mastodon from "src/icons/mastodon.svg?raw";
import facebook from "src/icons/facebook.svg?raw"; import facebook from "src/icons/facebook.svg?raw";
import rss from "src/icons/rss.svg?raw"; import rss from "src/icons/rss.svg?raw";
const icons = { discord, linkedin, twitter, mastodon, facebook, rss }; const icons: Record<string, string> = { discord, linkedin, twitter, mastodon, facebook, rss };
// Components used in the .MDX about files // Components used in the .MDX about files
// - see /content/site/about-us*.mdx for usages // - see /content/site/about-us*.mdx for usages

View File

@@ -15,13 +15,13 @@ export const themeToggle = () => {
const lightIconEls = document.querySelectorAll<HTMLElement>( const lightIconEls = document.querySelectorAll<HTMLElement>(
"[data-theme-toggle-icon='light']", "[data-theme-toggle-icon='light']",
); );
function toggleButton(theme) { function toggleButton(theme: string) {
themeToggleBtns.forEach((el) => (el.ariaPressed = `${theme === "dark"}`)); themeToggleBtns.forEach((el) => (el.ariaPressed = `${theme === "dark"}`));
lightIconEls.forEach((el) => { lightIconEls.forEach((el) => {
el.style.display = theme === "light" ? null : "none"; el.style.display = theme === "light" ? "" : "none";
}); });
darkIconEls.forEach((el) => { darkIconEls.forEach((el) => {
el.style.display = theme === "light" ? "none" : null; el.style.display = theme === "light" ? "none" : "";
}); });
// update the meta theme-color attribute(s) based on the user preference // update the meta theme-color attribute(s) based on the user preference

View File

@@ -4,7 +4,7 @@ export function getShortTitle(
post: PostInfo, post: PostInfo,
collection?: CollectionInfo, collection?: CollectionInfo,
): string { ): string {
const collectionTitle = collection?.title || post.collection; const collectionTitle = collection?.title || post.collection || "";
// if the post title starts with its collection title, remove it // if the post title starts with its collection title, remove it
if (post.title.startsWith(`${collectionTitle}: `)) if (post.title.startsWith(`${collectionTitle}: `))
return post.title.substring(collectionTitle.length + 2); return post.title.substring(collectionTitle.length + 2);

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck TODO
import { JSX } from "preact"; import { JSX } from "preact";
import { import {
useCallback, useCallback,

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck TODO
function throttle(callback, limit) { function throttle(callback, limit) {
let waiting = false; let waiting = false;
return function (...props) { return function (...props) {

View File

@@ -1,3 +1,6 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck TODO
// https://github.com/wuct/raf-throttle/blob/master/rafThrottle.js // https://github.com/wuct/raf-throttle/blob/master/rafThrottle.js
const rafThrottle = (callback) => { const rafThrottle = (callback) => {
let requestId = null; let requestId = null;

View File

@@ -90,7 +90,7 @@ const FilterDialogMobile = ({
count={author.numPosts} count={author.numPosts}
icon={ icon={
<UUPicture <UUPicture
picture={unicornProfilePicMap.find((u) => u.id === author.id)} picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
alt={""} alt={""}
class={styles.authorIcon} class={styles.authorIcon}
/> />
@@ -198,7 +198,7 @@ const FilterDialogSmallTablet = ({
<UUPicture <UUPicture
picture={unicornProfilePicMap.find( picture={unicornProfilePicMap.find(
(u) => u.id === author.id, (u) => u.id === author.id,
)} )!}
alt={""} alt={""}
class={styles.authorIcon} class={styles.authorIcon}
/> />
@@ -261,7 +261,7 @@ export const FilterDialog = ({
}; };
const onFormConfirm = useCallback( const onFormConfirm = useCallback(
(returnValue: string) => { (returnValue?: string) => {
// if the "confirm" button is pressed, the dialog should // if the "confirm" button is pressed, the dialog should
// close with the selected values // close with the selected values
if (returnValue === "confirm") { if (returnValue === "confirm") {

View File

@@ -8,9 +8,11 @@ import { useWindowSize } from "../../../hooks/use-window-size";
import { tabletLarge } from "../../../tokens/breakpoints"; import { tabletLarge } from "../../../tokens/breakpoints";
import { FilterDialog } from "./filter-dialog"; import { FilterDialog } from "./filter-dialog";
import { FilterSidebar } from "./filter-sidebar"; import { FilterSidebar } from "./filter-sidebar";
import tagMap from "../../../../content/data/tags.json"; import tagsObj from "../../../../content/data/tags.json";
import { SortType } from "./types"; import { SortType } from "./types";
const tagsMap = new Map(Object.entries(tagsObj));
interface FilterDisplayProps { interface FilterDisplayProps {
unicornProfilePicMap: ProfilePictureMap; unicornProfilePicMap: ProfilePictureMap;
posts: PostInfo[]; posts: PostInfo[];
@@ -73,9 +75,7 @@ export const FilterDisplay = ({
.map((tag) => ({ .map((tag) => ({
tag, tag,
numPosts: tagToPostNumMap.get(tag) || 0, numPosts: tagToPostNumMap.get(tag) || 0,
emoji: tagMap[tag]?.emoji, ...tagsMap.get(tag),
image: tagMap[tag]?.image,
displayName: tagMap[tag]?.displayName,
})); }));
}, [posts]); }, [posts]);

View File

@@ -42,7 +42,7 @@ export const FilterSection = ({
// When cleared, the focus needs to be passed to the heading button // When cleared, the focus needs to be passed to the heading button
// to avoid resetting to <body> when the clear button is removed from the DOM. // to avoid resetting to <body> when the clear button is removed from the DOM.
// https://github.com/unicorn-utterances/unicorn-utterances/issues/742 // https://github.com/unicorn-utterances/unicorn-utterances/issues/742
const buttonRef = useRef<HTMLButtonElement>(); const buttonRef = useRef<HTMLButtonElement | null>(null);
const handleClear = () => { const handleClear = () => {
onClear(); onClear();

View File

@@ -112,7 +112,7 @@ export const FilterSidebar = ({
count={author.numPosts} count={author.numPosts}
icon={ icon={
<UUPicture <UUPicture
picture={unicornProfilePicMap.find((u) => u.id === author.id)} picture={unicornProfilePicMap.find((u) => u.id === author.id)!}
alt={""} alt={""}
class={styles.authorIcon} class={styles.authorIcon}
/> />

View File

@@ -1,11 +1,9 @@
import { JSXNode } from "components/types"; import { JSXNode } from "components/types";
import styles from "./search-hero.module.scss"; import styles from "./search-hero.module.scss";
import tags from "../../../../content/data/tags.json"; import tags from "../../../../content/data/tags.json";
import { MutableRef } from "preact/hooks";
import { Ref } from "preact";
const stickers = Object.values(tags) const stickers = Object.values(tags)
.filter((tag) => !!tag["shownWithBranding"] && !!tag["image"]) .filter((tag) => "shownWithBranding" in tag && "image" in tag && tag.shownWithBranding)
.sort(() => 0.5 - Math.random()) as { image: string }[]; .sort(() => 0.5 - Math.random()) as { image: string }[];
const stickerTransforms = [ const stickerTransforms = [

View File

@@ -9,7 +9,7 @@ interface SearchResultCountProps {
} }
export const SearchResultCount = forwardRef< export const SearchResultCount = forwardRef<
HTMLDivElement, HTMLDivElement | null,
SearchResultCountProps SearchResultCountProps
>(({ numberOfPosts, numberOfCollections }, ref) => { >(({ numberOfPosts, numberOfCollections }, ref) => {
const language = useMemo(() => { const language = useMemo(() => {

View File

@@ -77,7 +77,7 @@ export const SearchTopbar = ({
type="submit" type="submit"
aria-label="Search" aria-label="Search"
dangerouslySetInnerHTML={{ __html: forward }} dangerouslySetInnerHTML={{ __html: forward }}
children={null} children={[]}
/> />
</form> </form>
<div className={style.bigScreenContainer} /> <div className={style.bigScreenContainer} />

View File

@@ -41,7 +41,7 @@ afterAll(() => server.close());
function mockFetch(fn: (searchStr: string) => ServerReturnType) { function mockFetch(fn: (searchStr: string) => ServerReturnType) {
server.use( server.use(
rest.get<ServerReturnType>(`/api/search`, async (req, res, ctx) => { rest.get<ServerReturnType>(`/api/search`, async (req, res, ctx) => {
const searchString = req.url.searchParams.get("query"); const searchString = req.url.searchParams.get("query")!;
return res(ctx.json(fn(searchString))); return res(ctx.json(fn(searchString)));
}), }),
); );
@@ -53,7 +53,9 @@ function mockFetchWithStatus(
) { ) {
server.use( server.use(
rest.get<never>(`/api/search`, async (req, res, ctx) => { rest.get<never>(`/api/search`, async (req, res, ctx) => {
const searchString = req.url.searchParams.get("query"); const searchString = req.url.searchParams.get("query")!;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore it's fine
return res(ctx.status(status), ctx.json(fn(searchString))); return res(ctx.status(status), ctx.json(fn(searchString)));
}), }),
); );
@@ -338,7 +340,7 @@ describe("Search page", () => {
const select = const select =
container instanceof HTMLSelectElement container instanceof HTMLSelectElement
? container ? container
: container.querySelector("select"); : container.querySelector("select")!;
await user.selectOptions(select, "newest"); await user.selectOptions(select, "newest");
@@ -398,7 +400,7 @@ describe("Search page", () => {
const select = const select =
container instanceof HTMLSelectElement container instanceof HTMLSelectElement
? container ? container
: container.querySelector("select"); : container.querySelector("select")!;
user.selectOptions(select, "newest"); user.selectOptions(select, "newest");

View File

@@ -101,7 +101,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
500, 500,
); );
const resultsHeading = useRef<HTMLDivElement>(); const resultsHeading = useRef<HTMLDivElement | null>(null);
const onManualSubmit = useCallback( const onManualSubmit = useCallback(
(str: string) => { (str: string) => {
@@ -139,7 +139,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
totalPosts: 0, totalPosts: 0,
collections: [], collections: [],
totalCollections: 0, totalCollections: 0,
}, } as ServerReturnType,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: false, retry: false,
enabled, enabled,
@@ -194,7 +194,7 @@ function SearchPageBase({ unicornProfilePicMap }: SearchPageProps) {
// Setup content to display // Setup content to display
const contentToDisplay = useMemo(() => { const contentToDisplay = useMemo(() => {
const urlVal = urlParams.get(CONTENT_TO_DISPLAY_KEY); const urlVal = urlParams.get(CONTENT_TO_DISPLAY_KEY);
const isValid = ["all", "articles", "collections"].includes(urlVal); const isValid = ["all", "articles", "collections"].includes(String(urlVal));
if (isValid) return urlVal as "all" | "articles" | "collections"; if (isValid) return urlVal as "all" | "articles" | "collections";
return DEFAULT_CONTENT_TO_DISPLAY; return DEFAULT_CONTENT_TO_DISPLAY;
}, [urlParams]); }, [urlParams]);

View File

@@ -10,6 +10,7 @@
// Enable stricter transpilation for better output. // Enable stricter transpilation for better output.
"isolatedModules": true, "isolatedModules": true,
"esModuleInterop": true, "esModuleInterop": true,
"strict": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"skipLibCheck": true, "skipLibCheck": true,
"jsxImportSource": "preact", "jsxImportSource": "preact",