CloudCannon / pagefind

Static low-bandwidth search at scale
https://pagefind.app
MIT License
3.34k stars 100 forks source link

Consider publishing the JS client as an NPM package #293

Open kamoshi opened 1 year ago

kamoshi commented 1 year ago

I've found that some of the biggest headaches while using Pagefind in a JS framework come from the fact that:

  1. It seems there is no way to statically import client from pagefind package. This makes it much harder to use Pagefind with build tools like Vite.
  2. There are no TypeScript types available. A package distributed via NPM could have an accompanying @types/pagefind package or have types included. It would make it easier to use the JS client even when not using TypeScript.

For example, I used Pagefind JS client in Solidjs via Astro, which uses Vite. The way I went about it was that I first wrote my own rudimentary types:

interface Pagefind {
  search: (query: string) => Promise<PagefindResponse>;
}

interface PagefindResponse {
  results: PagefindResult[];
}

interface PagefindResult {
  id: string;
  data: () => Promise<PagefindDocument>;
}

interface PagefindDocument {
  url: string;
  excerpt: string;
  filters: {
    author: string;
  };
  meta: {
    title: string;
    image: string;
  };
  content: string;
  word_count: number;
}

Then I had to import Pagefind in a way that would avoid Vite headaches:

async function loadPagefind(): Promise<Pagefind> {
  const pf = "/_pagefind/pagefind.js";
  return await import(/* @vite-ignore */ pf);
}

export default function Search() {
  const [query, setQuery] = createSignal('');

  // Search
  let pagefind: Pagefind;
  const [pages] = createResource(query, async (query: string) => await pagefind.search(query));

  // == snip ==

  onMount(async () => {
    pagefind = await loadPagefind();
    // == snip ==
  }

  return (
    <div>jsx here</div>
  )
}

After doing all this Pagefind does work correctly, but I'm getting a warning:

pagefind doesn't appear to be written in CJS, but also doesn't appear to be a valid ES module (i.e. it doesn't have "type": "module" or an .mjs extension for the entry point). Please contact the package author to fix.

bglw commented 1 year ago

πŸ‘‹ Hey @kamoshi

This is a good suggestion, I'll look into whether it's viable for us. I believe early models of Pagefind had a slightly more bespoke search bundle per-site, but now the version string is the only dynamic content, so in theory that file can be distributed elsewhere.

There are a couple of reasons I haven't pursued this yet:

  1. The /_pagefind/pagefind.js uses its own import path at runtime to locate the /_pagefind/pagefind-entry.json + the rest of the wasm files and search bundle. This has been handy for lowering the config barrier, but isn't that big of a deal in the grand scheme of things
  2. If they're distributed separately, it's inevitable that people will wind up pulling in a JS client at a different version than the Pagefind binary they're running. There's no semver guarantee between the JS client and the WASM, so a differing patch release might break the communications between the two. The current flow at least ensures that your Pagefind versioning doesn't get out of sync.

In saying that, those aren't good enough reasons to not do it at all! 1. isn't a blocker, but will just require some careful documentation. Regarding 2., perhaps a solution is publishing a wrapper package around the JS client to NPM. This would give you your typescript definitions and nice DX integrations, but ultimately under the hood will import("/_pagefind/pagefind.js") to interface with Pagefind, so that the WASM communication doesn't get baked into the NPM version. It should be transparent to the end-user, unless Vite digs deep and complains about the inner import πŸ€”

Let me know your thoughts on that πŸ™‚

arjanski commented 1 year ago

Just a +1 for me for @kamoshi 's feature requests, I think both an NPM package and types would greatly improve adoption and DX!

CapitaineToinon commented 8 months ago

I'd love too!

Just in case anyone is having the same issue, I'm using pagefind with the node api in a Remix + vite project and I had to use this syntax to be able to import:

export interface Pagefind {
  search: (query: string) => Promise<PagefindResponse>;
}

export interface PagefindResponse {
  results: PagefindResult[];
}

export interface PagefindResult {
  id: string;
  data: () => Promise<PagefindDocument>;
}

export interface PagefindDocument {
  url: string;
  raw_url: string;
  excerpt: string;
  // ... whatever else
}

declare global {
  interface Window {
    pagefind: Pagefind;
  }
}

export async function importPagefind(): Promise<Pagefind> {
  const url = new URL("/pagefind/pagefind.js", import.meta.url).href;
  return await import(/* @vite-ignore */ url);
}

And then load it with a useEffect

  useEffect(() => {
    async function loadPagefind() {
      if (typeof window.pagefind === "undefined") {
        try {
          window.pagefind = await importPagefind();
        } catch (e) {
          window.pagefind = {
            search: async () => ({ results: [] }),
          };
        }
      }
    }
    loadPagefind();
  }, []);

This will look for pagefind in the public directly so this works for both dev and production but you need to build pagefind first.