samedi 7 mars 2020

Preprocess code for React app that is only run at compile time

I am using react-static to compile a bunch of blog pages, each written as a separate .tsx file, into a static site. However, I want to have a special feature, which is to auto-generate specific content (e.g. list of links, reference text) based on the DOM tree of another page, but my current implementation works very inefficiently with react-static's workflow:

For example, to automatically extract a bunch of pages' section titles and link to them:

Metadata.tsx

export type SectionMetadata = {
  label: string;
  title: React.ReactNode;
};

export default class Metadata {
  static context = React.createContext(new Metadata());

  static Provider: React.FC<{ metadata?: Metadata }> = props => (
    <Metadata.context.Provider value={props.metadata || new Metadata()}>
      {props.children}
    </Metadata.context.Provider>
  );

  static extract(content: React.ReactNode) {
    const metadata = new Metadata();
    ReactDOMServer.renderToString(
      <Metadata.Provider metadata={metadata}>{content}</Metadata.Provider>
    );
    return metadata;
  }

  sections: SectionMetadata[] = [];

  registerSection = (label: string, title: React.ReactNode) => {
    this.sections.push({ label, title, subsections: [] });
  };
}

Section.tsx

export const Section: React.FC<{
  label: string;
  title: React.ReactNode;
}> = props => {
  const metadata = React.useContext(Metadata.context);
  const index = metadata.registerSection(props.label, props.title);
  return (
    <>
      <h1 id={props.label}>{props.title}</h1>
      {props.children}
    </>
  );
};

Index.tsx

type SectionData = { page: string; label: string; title: React.ReactNode };
const contents: SectionData[] = [];
const contentContext = require.context("/pages", true, /\.tsx$/, "lazy");
for (const name of contentContext.keys()) {
  const label = path.basename(name, ".tsx");
  contentContext(name).then(({ default: Page }: { default: any }) => {
    const metadata = Metadata.extract(<Page />);
    for (const sec of metadata.sections) {
      contents.push({ page: label, label: sec.label, title: sec.title });
    }
  });
}

// Omitted some delaying mechanism that allows `contents` to be
// initialized before rendering
export default class Index extends React.Component {
  render = () => (
    <ul>
      {contents.map((sec: SectionData, index: number) => (
        <li key={index}>
          <Link to={`/pages/${sec.page}#${sec.label}`}>{sec.title}</Link>
        </li>
      ))}
    </ul>
  );
}

In this way, all Section tags present in all pages will be captured and fed to Index.tsx to generate the list of links.

However, currently react-static treats this as part of the app behaviour and packs it inside the exported web app. This means that every time a client opens the Index page, the web app will attempt to download all pages and re-run the preprocessing logic. I wish to change this so that in the exported web app, the dynamically generated part is replaced by static links.

My blog pages consist of special layouts and environments using React components, so I have to stick with one React component per page and it will be very complicated to change to Markdown / plain HTML with templating language, etc. But I am okay with switching to Gatsby or Next.js if this feature can be implemented there.




Aucun commentaire:

Enregistrer un commentaire