kabirgh
2024-01-18 ce3dd0923b93e8c1cbe95fe584418d6ee5dcba69
refactor: move emit from callback to helper file function (#704)

* Change emit from callback to helpers file function

* Update docs, remove commented code, improve type sig
1 files added
13 files modified
123 ■■■■ changed files
docs/advanced/making plugins.md 15 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/404.tsx 6 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/aliases.ts 7 ●●●● patch | view | raw | blame | history
quartz/plugins/emitters/assets.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/emitters/cname.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/emitters/componentResources.ts 12 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentIndex.ts 12 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/contentPage.tsx 6 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/folderPage.tsx 6 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/helpers.ts 19 ●●●●● patch | view | raw | blame | history
quartz/plugins/emitters/static.ts 2 ●●● patch | view | raw | blame | history
quartz/plugins/emitters/tagPage.tsx 6 ●●●●● patch | view | raw | blame | history
quartz/plugins/types.ts 15 ●●●●● patch | view | raw | blame | history
quartz/processors/emit.ts 13 ●●●●● patch | view | raw | blame | history
docs/advanced/making plugins.md
@@ -216,22 +216,19 @@
export type QuartzEmitterPluginInstance = {
  name: string
  emit(
    ctx: BuildCtx,
    content: ProcessedContent[],
    resources: StaticResources,
    emitCallback: EmitCallback,
  ): Promise<FilePath[]>
  emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
  getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
}
```
An emitter plugin must define a `name` field an `emit` function and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
An emitter plugin must define a `name` field, an `emit` function, and a `getQuartzComponents` function. `emit` is responsible for looking at all the parsed and filtered content and then appropriately creating files and returning a list of paths to files the plugin created.
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `emitCallback` if you are creating files that contain text. The `emitCallback` function is the 4th argument of the emit function. Its interface looks something like this:
Creating new files can be done via regular Node [fs module](https://nodejs.org/api/fs.html) (i.e. `fs.cp` or `fs.writeFile`) or via the `write` function in `quartz/plugins/emitters/helpers.ts` if you are creating files that contain text. `write` has the following signature:
```ts
export type EmitCallback = (data: {
export type WriteOptions = (data: {
  // the build context
  ctx: BuildCtx
  // the name of the file to emit (not including the file extension)
  slug: ServerSlug
  // the file extension
quartz/plugins/emitters/404.tsx
@@ -7,6 +7,7 @@
import { sharedPageComponents } from "../../../quartz.layout"
import { NotFound } from "../../components"
import { defaultProcessedContent } from "../vfile"
import { write } from "./helpers"
export const NotFoundPage: QuartzEmitterPlugin = () => {
  const opts: FullPageLayout = {
@@ -25,7 +26,7 @@
    getQuartzComponents() {
      return [Head, Body, pageBody, Footer]
    },
    async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
    async emit(ctx, _content, resources): Promise<FilePath[]> {
      const cfg = ctx.cfg.configuration
      const slug = "404" as FullSlug
@@ -48,7 +49,8 @@
      }
      return [
        await emit({
        await write({
          ctx,
          content: renderPage(slug, componentData, opts, externalResources),
          slug,
          ext: ".html",
quartz/plugins/emitters/aliases.ts
@@ -1,13 +1,15 @@
import { FilePath, FullSlug, joinSegments, resolveRelative, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types"
import path from "path"
import { write } from "./helpers"
export const AliasRedirects: QuartzEmitterPlugin = () => ({
  name: "AliasRedirects",
  getQuartzComponents() {
    return []
  },
  async emit({ argv }, content, _resources, emit): Promise<FilePath[]> {
  async emit(ctx, content, _resources): Promise<FilePath[]> {
    const { argv } = ctx
    const fps: FilePath[] = []
    for (const [_tree, file] of content) {
@@ -32,7 +34,8 @@
        }
        const redirUrl = resolveRelative(slug, file.data.slug!)
        const fp = await emit({
        const fp = await write({
          ctx,
          content: `
            <!DOCTYPE html>
            <html lang="en-us">
quartz/plugins/emitters/assets.ts
@@ -10,7 +10,7 @@
    getQuartzComponents() {
      return []
    },
    async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
    async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
      // glob all non MD/MDX/HTML files in content folder and copy it over
      const assetsPath = argv.output
      const fps = await glob("**", argv.directory, ["**/*.md", ...cfg.configuration.ignorePatterns])
quartz/plugins/emitters/cname.ts
@@ -13,7 +13,7 @@
  getQuartzComponents() {
    return []
  },
  async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
  async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
    if (!cfg.configuration.baseUrl) {
      console.warn(chalk.yellow("CNAME emitter requires `baseUrl` to be set in your configuration"))
      return []
quartz/plugins/emitters/componentResources.ts
@@ -13,6 +13,7 @@
import { googleFontHref, joinStyles } from "../../util/theme"
import { Features, transform } from "lightningcss"
import { transform as transpile } from "esbuild"
import { write } from "./helpers"
type ComponentResources = {
  css: string[]
@@ -168,7 +169,7 @@
    getQuartzComponents() {
      return []
    },
    async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
    async emit(ctx, _content, resources): Promise<FilePath[]> {
      // component specific scripts and styles
      const componentResources = getComponentResources(ctx)
      // important that this goes *after* component scripts
@@ -190,7 +191,8 @@
      ])
      const fps = await Promise.all([
        emit({
        write({
          ctx,
          slug: "index" as FullSlug,
          ext: ".css",
          content: transform({
@@ -207,12 +209,14 @@
            include: Features.MediaQueries,
          }).code.toString(),
        }),
        emit({
        write({
          ctx,
          slug: "prescript" as FullSlug,
          ext: ".js",
          content: prescript,
        }),
        emit({
        write({
          ctx,
          slug: "postscript" as FullSlug,
          ext: ".js",
          content: postscript,
quartz/plugins/emitters/contentIndex.ts
@@ -6,6 +6,7 @@
import { QuartzEmitterPlugin } from "../types"
import { toHtml } from "hast-util-to-html"
import path from "path"
import { write } from "./helpers"
export type ContentIndex = Map<FullSlug, ContentDetails>
export type ContentDetails = {
@@ -91,7 +92,7 @@
  opts = { ...defaultOptions, ...opts }
  return {
    name: "ContentIndex",
    async emit(ctx, content, _resources, emit) {
    async emit(ctx, content, _resources) {
      const cfg = ctx.cfg.configuration
      const emitted: FilePath[] = []
      const linkIndex: ContentIndex = new Map()
@@ -115,7 +116,8 @@
      if (opts?.enableSiteMap) {
        emitted.push(
          await emit({
          await write({
            ctx,
            content: generateSiteMap(cfg, linkIndex),
            slug: "sitemap" as FullSlug,
            ext: ".xml",
@@ -125,7 +127,8 @@
      if (opts?.enableRSS) {
        emitted.push(
          await emit({
          await write({
            ctx,
            content: generateRSSFeed(cfg, linkIndex, opts.rssLimit),
            slug: "index" as FullSlug,
            ext: ".xml",
@@ -146,7 +149,8 @@
      )
      emitted.push(
        await emit({
        await write({
          ctx,
          content: JSON.stringify(simplifiedIndex),
          slug: fp,
          ext: ".json",
quartz/plugins/emitters/contentPage.tsx
@@ -8,6 +8,7 @@
import { defaultContentPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { Content } from "../../components"
import chalk from "chalk"
import { write } from "./helpers"
export const ContentPage: QuartzEmitterPlugin<Partial<FullPageLayout>> = (userOpts) => {
  const opts: FullPageLayout = {
@@ -26,7 +27,7 @@
    getQuartzComponents() {
      return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
    },
    async emit(ctx, content, resources, emit): Promise<FilePath[]> {
    async emit(ctx, content, resources): Promise<FilePath[]> {
      const cfg = ctx.cfg.configuration
      const fps: FilePath[] = []
      const allFiles = content.map((c) => c[1].data)
@@ -49,7 +50,8 @@
        }
        const content = renderPage(slug, componentData, opts, externalResources)
        const fp = await emit({
        const fp = await write({
          ctx,
          content,
          slug,
          ext: ".html",
quartz/plugins/emitters/folderPage.tsx
@@ -17,6 +17,7 @@
} from "../../util/path"
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { FolderContent } from "../../components"
import { write } from "./helpers"
export const FolderPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
  const opts: FullPageLayout = {
@@ -35,7 +36,7 @@
    getQuartzComponents() {
      return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
    },
    async emit(ctx, content, resources, emit): Promise<FilePath[]> {
    async emit(ctx, content, resources): Promise<FilePath[]> {
      const fps: FilePath[] = []
      const allFiles = content.map((c) => c[1].data)
      const cfg = ctx.cfg.configuration
@@ -82,7 +83,8 @@
        }
        const content = renderPage(slug, componentData, opts, externalResources)
        const fp = await emit({
        const fp = await write({
          ctx,
          content,
          slug,
          ext: ".html",
quartz/plugins/emitters/helpers.ts
New file
@@ -0,0 +1,19 @@
import path from "path"
import fs from "fs"
import { BuildCtx } from "../../util/ctx"
import { FilePath, FullSlug, joinSegments } from "../../util/path"
type WriteOptions = {
  ctx: BuildCtx
  slug: FullSlug
  ext: `.${string}` | ""
  content: string
}
export const write = async ({ ctx, slug, ext, content }: WriteOptions): Promise<FilePath> => {
  const pathToPage = joinSegments(ctx.argv.output, slug + ext) as FilePath
  const dir = path.dirname(pathToPage)
  await fs.promises.mkdir(dir, { recursive: true })
  await fs.promises.writeFile(pathToPage, content)
  return pathToPage
}
quartz/plugins/emitters/static.ts
@@ -8,7 +8,7 @@
  getQuartzComponents() {
    return []
  },
  async emit({ argv, cfg }, _content, _resources, _emit): Promise<FilePath[]> {
  async emit({ argv, cfg }, _content, _resources): Promise<FilePath[]> {
    const staticPath = joinSegments(QUARTZ, "static")
    const fps = await glob("**", staticPath, cfg.configuration.ignorePatterns)
    await fs.promises.cp(staticPath, joinSegments(argv.output, "static"), {
quartz/plugins/emitters/tagPage.tsx
@@ -14,6 +14,7 @@
} from "../../util/path"
import { defaultListPageLayout, sharedPageComponents } from "../../../quartz.layout"
import { TagContent } from "../../components"
import { write } from "./helpers"
export const TagPage: QuartzEmitterPlugin<FullPageLayout> = (userOpts) => {
  const opts: FullPageLayout = {
@@ -32,7 +33,7 @@
    getQuartzComponents() {
      return [Head, Header, Body, ...header, ...beforeBody, pageBody, ...left, ...right, Footer]
    },
    async emit(ctx, content, resources, emit): Promise<FilePath[]> {
    async emit(ctx, content, resources): Promise<FilePath[]> {
      const fps: FilePath[] = []
      const allFiles = content.map((c) => c[1].data)
      const cfg = ctx.cfg.configuration
@@ -81,7 +82,8 @@
        }
        const content = renderPage(slug, componentData, opts, externalResources)
        const fp = await emit({
        const fp = await write({
          ctx,
          content,
          slug: file.data.slug!,
          ext: ".html",
quartz/plugins/types.ts
@@ -36,19 +36,6 @@
) => QuartzEmitterPluginInstance
export type QuartzEmitterPluginInstance = {
  name: string
  emit(
    ctx: BuildCtx,
    content: ProcessedContent[],
    resources: StaticResources,
    emitCallback: EmitCallback,
  ): Promise<FilePath[]>
  emit(ctx: BuildCtx, content: ProcessedContent[], resources: StaticResources): Promise<FilePath[]>
  getQuartzComponents(ctx: BuildCtx): QuartzComponent[]
}
export interface EmitOptions {
  slug: FullSlug
  ext: `.${string}` | ""
  content: string
}
export type EmitCallback = (data: EmitOptions) => Promise<FilePath>
quartz/processors/emit.ts
@@ -1,10 +1,6 @@
import path from "path"
import fs from "fs"
import { PerfTimer } from "../util/perf"
import { getStaticResourcesFromPlugins } from "../plugins"
import { EmitCallback } from "../plugins/types"
import { ProcessedContent } from "../plugins/vfile"
import { FilePath, joinSegments } from "../util/path"
import { QuartzLogger } from "../util/log"
import { trace } from "../util/trace"
import { BuildCtx } from "../util/ctx"
@@ -15,19 +11,12 @@
  const log = new QuartzLogger(ctx.argv.verbose)
  log.start(`Emitting output files`)
  const emit: EmitCallback = async ({ slug, ext, content }) => {
    const pathToPage = joinSegments(argv.output, slug + ext) as FilePath
    const dir = path.dirname(pathToPage)
    await fs.promises.mkdir(dir, { recursive: true })
    await fs.promises.writeFile(pathToPage, content)
    return pathToPage
  }
  let emittedFiles = 0
  const staticResources = getStaticResourcesFromPlugins(ctx)
  for (const emitter of cfg.plugins.emitters) {
    try {
      const emitted = await emitter.emit(ctx, content, staticResources, emit)
      const emitted = await emitter.emit(ctx, content, staticResources)
      emittedFiles += emitted.length
      if (ctx.argv.verbose) {