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 exportedIndex from "../searchIndex";
import unicornProfilePicMap from "../public/unicorn-profile-pic-map";
import Fuse from "fuse.js";
import type { PostInfo } from "../src/types/PostInfo";
const myIndex = Fuse.parseIndex(exportedIndex.index);
@@ -18,11 +20,19 @@ const fuse = new Fuse(
myIndex
);
const unicornProfilePicObj = {};
for (const picMapItem of unicornProfilePicMap) {
unicornProfilePicObj[picMapItem.id] = picMapItem;
}
export default async (req: VercelRequest, res: VercelResponse) => {
// TODO: `pickdeep` only required fields
const searchStr = req?.query?.query as string;
if (!searchStr) return [];
if (Array.isArray(searchStr)) return [];
const items = fuse.search(searchStr).map((item) => item.item);
res.send(items);
const posts = fuse.search(searchStr).map((item) => item.item as PostInfo);
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 { rehypeWordCount } from "./src/utils/markdown/rehype-word-count";
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 preact from "@astrojs/preact";
@@ -25,6 +26,7 @@ import behead from "remark-behead";
import rehypeRaw from "rehype-raw";
import image from "@astrojs/image";
import path from "path";
export default defineConfig({
integrations: [image(), preact()],
@@ -46,6 +48,12 @@ export default defineConfig({
}),
enforce: "pre",
},
{
...generateUnicornProfilePicMap({
output: path.resolve("./public/unicorn-profile-pic-map.ts"),
}),
enforce: "pre",
},
],
},
markdown: {

1
public/.gitignore vendored
View File

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

View File

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

View File

@@ -1,37 +1,6 @@
import { getPicture } from "@astrojs/image";
import { unicorns } from "utils/data";
import unicornProfilePicMap from "../../public/unicorn-profile-pic-map";
/**
* 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 () => {
/**
* 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;
};

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;