chore: migrate unicorn profile pic mapping to Rollup plugin

This commit is contained in:
Corbin Crutchley
2022-09-25 08:49:34 -07:00
parent 1ef7a44c3b
commit 9df6f90a5e
7 changed files with 107 additions and 63 deletions

View File

@@ -1,7 +1,9 @@
import type { VercelRequest, VercelResponse } from "@vercel/node"; import type { VercelRequest, VercelResponse } from "@vercel/node";
import exportedIndex from "../searchIndex"; import exportedIndex from "../searchIndex";
import unicornProfilePicMap from "../public/unicorn-profile-pic-map";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import type { PostInfo } from "../src/types/PostInfo";
const myIndex = Fuse.parseIndex(exportedIndex.index); const myIndex = Fuse.parseIndex(exportedIndex.index);
@@ -18,11 +20,19 @@ const fuse = new Fuse(
myIndex myIndex
); );
const unicornProfilePicObj = {};
for (const picMapItem of unicornProfilePicMap) {
unicornProfilePicObj[picMapItem.id] = picMapItem;
}
export default async (req: VercelRequest, res: VercelResponse) => { export default async (req: VercelRequest, res: VercelResponse) => {
// TODO: `pickdeep` only required fields // TODO: `pickdeep` only required fields
const searchStr = req?.query?.query as string; const searchStr = req?.query?.query as string;
if (!searchStr) return []; if (!searchStr) return [];
if (Array.isArray(searchStr)) return []; if (Array.isArray(searchStr)) return [];
const items = fuse.search(searchStr).map((item) => item.item); const posts = fuse.search(searchStr).map((item) => item.item as PostInfo);
res.send(items); const unicornProfilePicMap = posts.flatMap((post) =>
post.authorsMeta.map((authorMeta) => unicornProfilePicObj[authorMeta.id])
);
res.send({ posts, totalPosts: posts.length, unicornProfilePicMap });
}; };

View File

@@ -17,6 +17,7 @@ import { rehypeExcerpt } from "./src/utils/markdown/rehype-excerpt";
import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post"; import { rehypeUnicornPopulatePost } from "./src/utils/markdown/rehype-unicorn-populate-post";
import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count"; import { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts"; import { rehypeUnicornGetSuggestedPosts } from "./src/utils/markdown/rehype-unicorn-get-suggested-posts";
import generateUnicornProfilePicMap from "./src/utils/rollup/generate-unicorn-profile-pic-map";
import copy from "rollup-plugin-copy"; import copy from "rollup-plugin-copy";
import preact from "@astrojs/preact"; import preact from "@astrojs/preact";
@@ -25,6 +26,7 @@ import behead from "remark-behead";
import rehypeRaw from "rehype-raw"; import rehypeRaw from "rehype-raw";
import image from "@astrojs/image"; import image from "@astrojs/image";
import path from "path";
export default defineConfig({ export default defineConfig({
integrations: [image(), preact()], integrations: [image(), preact()],
@@ -46,6 +48,12 @@ export default defineConfig({
}), }),
enforce: "pre", enforce: "pre",
}, },
{
...generateUnicornProfilePicMap({
output: path.resolve("./public/unicorn-profile-pic-map.ts"),
}),
enforce: "pre",
},
], ],
}, },
markdown: { markdown: {

1
public/.gitignore vendored
View File

@@ -1 +1,2 @@
content/** content/**
unicorn-profile-pic-map.ts

View File

@@ -3,12 +3,6 @@ import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
import styles from "./filter-search-bar.module.scss"; import styles from "./filter-search-bar.module.scss";
import SearchField from "./search-field/search-field.astro"; import SearchField from "./search-field/search-field.astro";
// import FilterListbox from "./filter-listbox/filter-listbox.astro"; // import FilterListbox from "./filter-listbox/filter-listbox.astro";
export interface FilterSearchBarProps {
unicornProfilePicMap: ProfilePictureMap;
}
const { unicornProfilePicMap } = Astro.props as FilterSearchBarProps;
--- ---
<div class={styles.iconContainer}> <div class={styles.iconContainer}>
@@ -18,14 +12,11 @@ const { unicornProfilePicMap } = Astro.props as FilterSearchBarProps;
</div> </div>
<!-- <FilterListbox class={styles.filterField} /> --> <!-- <FilterListbox class={styles.filterField} /> -->
</div> </div>
<script define:vars={{ unicornProfilePicMap }}>
window.unicornProfilePicMap =
window.unicornProfilePicMap || unicornProfilePicMap;
</script>
<script> <script>
import { render, createElement, Fragment } from "preact"; import { render, createElement, Fragment } from "preact";
import { PostInfo } from "types/PostInfo"; import { PostInfo } from "types/PostInfo";
import { debounce } from "utils/debounce"; import { debounce } from "utils/debounce";
import { ProfilePictureMap } from "utils/get-unicorn-profile-pic-map";
import { PostCard } from "../../components/post-card/post-card"; import { PostCard } from "../../components/post-card/post-card";
const searchInput: HTMLElement = document.querySelector("#search-input"); const searchInput: HTMLElement = document.querySelector("#search-input");
@@ -46,7 +37,6 @@ const { unicornProfilePicMap } = Astro.props as FilterSearchBarProps;
let abortController: AbortController | undefined; let abortController: AbortController | undefined;
function doSearch(val) { function doSearch(val) {
const unicornProfilePicMap = (window as any).unicornProfilePicMap || [];
if (abortController) { if (abortController) {
abortController.abort(); abortController.abort();
abortController = undefined; abortController = undefined;
@@ -54,23 +44,28 @@ const { unicornProfilePicMap } = Astro.props as FilterSearchBarProps;
abortController = new AbortController(); abortController = new AbortController();
fetch(`/api/search?query=${val}`, { signal: abortController.signal }) fetch(`/api/search?query=${val}`, { signal: abortController.signal })
.then((res) => res.json()) .then((res) => res.json())
.then((serverVal: PostInfo[]) => { .then(
// TODO: Debounce to avoid server kill (serverVal: {
const FilledPostCard = createElement( posts: PostInfo[];
Fragment, totalPosts: number;
{}, unicornProfilePicMap: ProfilePictureMap;
serverVal.map((post) => }) => {
createElement(PostCard, { const FilledPostCard = createElement(
unicornProfilePicMap, Fragment,
post, {},
}) serverVal.posts.map((post) =>
) createElement(PostCard, {
); unicornProfilePicMap: serverVal.unicornProfilePicMap,
render(FilledPostCard, searchPostList); post,
loadingEl && loadingEl.remove(); })
loadingEl = undefined; )
abortController = undefined; );
}); render(FilledPostCard, searchPostList);
loadingEl && loadingEl.remove();
loadingEl = undefined;
abortController = undefined;
}
);
} }
const debounceDoSearch = debounce(doSearch, 1000, false); const debounceDoSearch = debounce(doSearch, 1000, false);

View File

@@ -35,7 +35,7 @@ const unicornProfilePicMap = await getUnicornProfilePicMap();
> --> > -->
<PostListHeader siteDescription={siteMetadata.description} /> <PostListHeader siteDescription={siteMetadata.description} />
<main> <main>
<FilterSearchBar unicornProfilePicMap={unicornProfilePicMap} /> <FilterSearchBar />
<PostList <PostList
listAriaLabel="List of posts" listAriaLabel="List of posts"
postsToDisplay={posts} postsToDisplay={posts}

View File

@@ -1,37 +1,6 @@
import { getPicture } from "@astrojs/image"; import unicornProfilePicMap from "../../public/unicorn-profile-pic-map";
import { unicorns } from "utils/data";
/**
* TODO: THIS IS A MAJOR OPTIMIZATION (15KB on every page load), READ THIS
*
* Right now, we're storing an inlined script of `getPicture` into the HTML of the page because Astro cannot
* use both `defer` external scripts, and `declare:vars`. This means that 15KB of this profile pic mapping is externalized currently.
*
* To fix this, instead of generating this at Astro build time, we should do this during an earlier compile step that simply runs
* this `getPicture` code and then outputs to a JS file, then the Astro build will read from this JS file
*/
export const getUnicornProfilePicMap = async () => { export const getUnicornProfilePicMap = async () => {
/**
* We do it this was so that we only generate the list of images once
*
* This allows us to share the cached image format between multiple different pages
*/
globalThis.unicornProfilePicMap =
globalThis.unicornProfilePicMap ||
(await Promise.all(
unicorns.map(async (unicorn) => ({
...(await getPicture({
src: unicorn.profileImgMeta.relativeServerPath,
formats: ["webp", "png"],
widths: [72, 48],
aspectRatio: 1,
})),
id: unicorn.id,
}))
));
const unicornProfilePicMap: Array<
Awaited<ReturnType<typeof getPicture>> & { id: string }
> = globalThis.unicornProfilePicMap;
return unicornProfilePicMap; return unicornProfilePicMap;
}; };

View File

@@ -0,0 +1,61 @@
import fs from "fs";
import { unicorns } from "../data";
/**
* They need to be the same `getImage` with the same `globalThis` instance, thanks to the "hack" workaround.
*/
import { getPicture } from "../../../node_modules/@astrojs/image";
import sharp_service from "../../../node_modules/@astrojs/image/dist/loaders/sharp.js";
interface GenerateUnicornProfilePicMapProps {
output: string;
}
function generateUnicornProfilePicMap(
options: GenerateUnicornProfilePicMapProps
) {
const { output } = options;
let copied = false;
return {
name: "generateUnicornProfilePicMap",
options: async () => {
// Only run once per dev HMR instance
if (copied) {
return;
}
// HACK: This is a hack that heavily relies on `getImage`'s internals :(
globalThis.astroImage = {
...(globalThis.astroImage || {}),
loader: sharp_service ?? globalThis.astroImage?.loader,
};
/**
* We do it this was so that we only generate the list of images once
*
* This allows us to share the cached image format between multiple different pages
*/
const unicornProfilePicMap = await Promise.all(
unicorns.map(async (unicorn) => ({
...(await getPicture({
src: unicorn.profileImgMeta.relativeServerPath,
formats: ["webp", "png"],
widths: [72, 48],
aspectRatio: 1,
})),
id: unicorn.id,
}))
);
const js = `const data = ${JSON.stringify(unicornProfilePicMap)};
export default data;`;
await fs.promises.writeFile(output, js);
copied = true;
},
};
}
export default generateUnicornProfilePicMap;