make component resources a proper emitter
This commit is contained in:
		
							parent
							
								
									236ba56be1
								
							
						
					
					
						commit
						4811500b1b
					
				
					 7 changed files with 163 additions and 177 deletions
				
			
		| 
						 | 
				
			
			@ -4,7 +4,6 @@ draft: true
 | 
			
		|||
 | 
			
		||||
## high priority
 | 
			
		||||
 | 
			
		||||
- component resources should be emitted by an emitter
 | 
			
		||||
- https://help.obsidian.md/Editing+and+formatting/Tags#Nested+tags nested tags??
 | 
			
		||||
- watch mode for config/source code
 | 
			
		||||
- https://help.obsidian.md/Editing+and+formatting/Basic+formatting+syntax#Task+lists task list styling
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,6 +95,7 @@ const config: QuartzConfig = {
 | 
			
		|||
    filters: [Plugin.RemoveDrafts()],
 | 
			
		||||
    emitters: [
 | 
			
		||||
      Plugin.AliasRedirects(),
 | 
			
		||||
      Plugin.ComponentResources(),
 | 
			
		||||
      Plugin.ContentPage({
 | 
			
		||||
        ...sharedPageComponents,
 | 
			
		||||
        ...contentPageLayout,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -82,8 +82,8 @@ const BuildArgv = {
 | 
			
		|||
  bundleInfo: {
 | 
			
		||||
    boolean: true,
 | 
			
		||||
    default: false,
 | 
			
		||||
    describe: "show detailed bundle information"
 | 
			
		||||
  }
 | 
			
		||||
    describe: "show detailed bundle information",
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function escapePath(fp) {
 | 
			
		||||
| 
						 | 
				
			
			@ -351,9 +351,9 @@ See the [documentation](https://quartz.jzhao.xyz) for how to get started.
 | 
			
		|||
      console.log(
 | 
			
		||||
        `Successfully transpiled ${Object.keys(meta.inputs).length} files (${prettyBytes(
 | 
			
		||||
          meta.bytes,
 | 
			
		||||
        )})`)
 | 
			
		||||
      console.log(await esbuild.analyzeMetafile(result.metafile, { color: true })
 | 
			
		||||
        )})`,
 | 
			
		||||
      )
 | 
			
		||||
      console.log(await esbuild.analyzeMetafile(result.metafile, { color: true }))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const { default: buildQuartz } = await import(cacheFile)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										154
									
								
								quartz/plugins/emitters/componentResources.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										154
									
								
								quartz/plugins/emitters/componentResources.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,154 @@
 | 
			
		|||
import { FilePath, ServerSlug } from "../../path"
 | 
			
		||||
import { PluginTypes, QuartzEmitterPlugin } from "../types"
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import spaRouterScript from "../../components/scripts/spa.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import plausibleScript from "../../components/scripts/plausible.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import popoverScript from "../../components/scripts/popover.inline"
 | 
			
		||||
import styles from "../../styles/base.scss"
 | 
			
		||||
import popoverStyle from "../../components/styles/popover.scss"
 | 
			
		||||
import { BuildCtx } from "../../ctx"
 | 
			
		||||
import { StaticResources } from "../../resources"
 | 
			
		||||
import { QuartzComponent } from "../../components/types"
 | 
			
		||||
import { googleFontHref, joinStyles } from "../../theme"
 | 
			
		||||
 | 
			
		||||
type ComponentResources = {
 | 
			
		||||
  css: string[]
 | 
			
		||||
  beforeDOMLoaded: string[]
 | 
			
		||||
  afterDOMLoaded: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getComponentResources(plugins: PluginTypes): ComponentResources {
 | 
			
		||||
  const allComponents: Set<QuartzComponent> = new Set()
 | 
			
		||||
  for (const emitter of plugins.emitters) {
 | 
			
		||||
    const components = emitter.getQuartzComponents()
 | 
			
		||||
    for (const component of components) {
 | 
			
		||||
      allComponents.add(component)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const componentResources = {
 | 
			
		||||
    css: new Set<string>(),
 | 
			
		||||
    beforeDOMLoaded: new Set<string>(),
 | 
			
		||||
    afterDOMLoaded: new Set<string>(),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const component of allComponents) {
 | 
			
		||||
    const { css, beforeDOMLoaded, afterDOMLoaded } = component
 | 
			
		||||
    if (css) {
 | 
			
		||||
      componentResources.css.add(css)
 | 
			
		||||
    }
 | 
			
		||||
    if (beforeDOMLoaded) {
 | 
			
		||||
      componentResources.beforeDOMLoaded.add(beforeDOMLoaded)
 | 
			
		||||
    }
 | 
			
		||||
    if (afterDOMLoaded) {
 | 
			
		||||
      componentResources.afterDOMLoaded.add(afterDOMLoaded)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    css: [...componentResources.css],
 | 
			
		||||
    beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
 | 
			
		||||
    afterDOMLoaded: [...componentResources.afterDOMLoaded],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function joinScripts(scripts: string[]): string {
 | 
			
		||||
  // wrap with iife to prevent scope collision
 | 
			
		||||
  return scripts.map((script) => `(function () {${script}})();`).join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function addGlobalPageResources(
 | 
			
		||||
  ctx: BuildCtx,
 | 
			
		||||
  staticResources: StaticResources,
 | 
			
		||||
  componentResources: ComponentResources,
 | 
			
		||||
) {
 | 
			
		||||
  const cfg = ctx.cfg.configuration
 | 
			
		||||
  const reloadScript = ctx.argv.serve
 | 
			
		||||
  staticResources.css.push(googleFontHref(cfg.theme))
 | 
			
		||||
 | 
			
		||||
  // popovers
 | 
			
		||||
  if (cfg.enablePopovers) {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(popoverScript)
 | 
			
		||||
    componentResources.css.push(popoverStyle)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (cfg.analytics?.provider === "google") {
 | 
			
		||||
    const tagId = cfg.analytics.tagId
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
 | 
			
		||||
      contentType: "external",
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
    })
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
      window.dataLayer = window.dataLayer || [];
 | 
			
		||||
      function gtag() { dataLayer.push(arguments); }
 | 
			
		||||
      gtag(\`js\`, new Date());
 | 
			
		||||
      gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
 | 
			
		||||
  
 | 
			
		||||
      document.addEventListener(\`nav\`, () => {
 | 
			
		||||
        gtag(\`event\`, \`page_view\`, {
 | 
			
		||||
          page_title: document.title,
 | 
			
		||||
          page_location: location.href,
 | 
			
		||||
        });
 | 
			
		||||
      });`)
 | 
			
		||||
  } else if (cfg.analytics?.provider === "plausible") {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(plausibleScript)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // spa
 | 
			
		||||
  if (cfg.enableSPA) {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(spaRouterScript)
 | 
			
		||||
  } else {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
        window.spaNavigate = (url, _) => window.location.assign(url)
 | 
			
		||||
        const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
 | 
			
		||||
        document.dispatchEvent(event)`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (reloadScript) {
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
      contentType: "inline",
 | 
			
		||||
      script: `
 | 
			
		||||
          const socket = new WebSocket('ws://localhost:3001')
 | 
			
		||||
          socket.addEventListener('message', () => document.location.reload())
 | 
			
		||||
        `,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const ComponentResources: QuartzEmitterPlugin = () => ({
 | 
			
		||||
  name: "ComponentResources",
 | 
			
		||||
  getQuartzComponents() {
 | 
			
		||||
    return []
 | 
			
		||||
  },
 | 
			
		||||
  async emit(ctx, _content, resources, emit): Promise<FilePath[]> {
 | 
			
		||||
    // component specific scripts and styles
 | 
			
		||||
    const componentResources = getComponentResources(ctx.cfg.plugins)
 | 
			
		||||
    // important that this goes *after* component scripts
 | 
			
		||||
    // as the "nav" event gets triggered here and we should make sure
 | 
			
		||||
    // that everyone else had the chance to register a listener for it
 | 
			
		||||
    addGlobalPageResources(ctx, resources, componentResources)
 | 
			
		||||
    const fps = await Promise.all([
 | 
			
		||||
      emit({
 | 
			
		||||
        slug: "index" as ServerSlug,
 | 
			
		||||
        ext: ".css",
 | 
			
		||||
        content: joinStyles(ctx.cfg.configuration.theme, styles, ...componentResources.css),
 | 
			
		||||
      }),
 | 
			
		||||
      emit({
 | 
			
		||||
        slug: "prescript" as ServerSlug,
 | 
			
		||||
        ext: ".js",
 | 
			
		||||
        content: joinScripts(componentResources.beforeDOMLoaded),
 | 
			
		||||
      }),
 | 
			
		||||
      emit({
 | 
			
		||||
        slug: "postscript" as ServerSlug,
 | 
			
		||||
        ext: ".js",
 | 
			
		||||
        content: joinScripts(componentResources.afterDOMLoaded),
 | 
			
		||||
      }),
 | 
			
		||||
    ])
 | 
			
		||||
    return fps
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
| 
						 | 
				
			
			@ -5,3 +5,4 @@ export { ContentIndex } from "./contentIndex"
 | 
			
		|||
export { AliasRedirects } from "./aliases"
 | 
			
		||||
export { Assets } from "./assets"
 | 
			
		||||
export { Static } from "./static"
 | 
			
		||||
export { ComponentResources } from "./componentResources"
 | 
			
		||||
| 
						 | 
				
			
			@ -1,82 +1,7 @@
 | 
			
		|||
import { GlobalConfiguration } from "../cfg"
 | 
			
		||||
import { QuartzComponent } from "../components/types"
 | 
			
		||||
import { StaticResources } from "../resources"
 | 
			
		||||
import { joinStyles } from "../theme"
 | 
			
		||||
import { EmitCallback, PluginTypes } from "./types"
 | 
			
		||||
import styles from "../styles/base.scss"
 | 
			
		||||
import { PluginTypes } from "./types"
 | 
			
		||||
import { FilePath, ServerSlug } from "../path"
 | 
			
		||||
 | 
			
		||||
export type ComponentResources = {
 | 
			
		||||
  css: string[]
 | 
			
		||||
  beforeDOMLoaded: string[]
 | 
			
		||||
  afterDOMLoaded: string[]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getComponentResources(plugins: PluginTypes): ComponentResources {
 | 
			
		||||
  const allComponents: Set<QuartzComponent> = new Set()
 | 
			
		||||
  for (const emitter of plugins.emitters) {
 | 
			
		||||
    const components = emitter.getQuartzComponents()
 | 
			
		||||
    for (const component of components) {
 | 
			
		||||
      allComponents.add(component)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const componentResources = {
 | 
			
		||||
    css: new Set<string>(),
 | 
			
		||||
    beforeDOMLoaded: new Set<string>(),
 | 
			
		||||
    afterDOMLoaded: new Set<string>(),
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (const component of allComponents) {
 | 
			
		||||
    const { css, beforeDOMLoaded, afterDOMLoaded } = component
 | 
			
		||||
    if (css) {
 | 
			
		||||
      componentResources.css.add(css)
 | 
			
		||||
    }
 | 
			
		||||
    if (beforeDOMLoaded) {
 | 
			
		||||
      componentResources.beforeDOMLoaded.add(beforeDOMLoaded)
 | 
			
		||||
    }
 | 
			
		||||
    if (afterDOMLoaded) {
 | 
			
		||||
      componentResources.afterDOMLoaded.add(afterDOMLoaded)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return {
 | 
			
		||||
    css: [...componentResources.css],
 | 
			
		||||
    beforeDOMLoaded: [...componentResources.beforeDOMLoaded],
 | 
			
		||||
    afterDOMLoaded: [...componentResources.afterDOMLoaded],
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function joinScripts(scripts: string[]): string {
 | 
			
		||||
  // wrap with iife to prevent scope collision
 | 
			
		||||
  return scripts.map((script) => `(function () {${script}})();`).join("\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function emitComponentResources(
 | 
			
		||||
  cfg: GlobalConfiguration,
 | 
			
		||||
  res: ComponentResources,
 | 
			
		||||
  emit: EmitCallback,
 | 
			
		||||
): Promise<FilePath[]> {
 | 
			
		||||
  const fps = await Promise.all([
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "index" as ServerSlug,
 | 
			
		||||
      ext: ".css",
 | 
			
		||||
      content: joinStyles(cfg.theme, styles, ...res.css),
 | 
			
		||||
    }),
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "prescript" as ServerSlug,
 | 
			
		||||
      ext: ".js",
 | 
			
		||||
      content: joinScripts(res.beforeDOMLoaded),
 | 
			
		||||
    }),
 | 
			
		||||
    emit({
 | 
			
		||||
      slug: "postscript" as ServerSlug,
 | 
			
		||||
      ext: ".js",
 | 
			
		||||
      content: joinScripts(res.afterDOMLoaded),
 | 
			
		||||
    }),
 | 
			
		||||
  ])
 | 
			
		||||
  return fps
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getStaticResourcesFromPlugins(plugins: PluginTypes) {
 | 
			
		||||
  const staticResources: StaticResources = {
 | 
			
		||||
    css: [],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,89 +1,14 @@
 | 
			
		|||
import path from "path"
 | 
			
		||||
import fs from "fs"
 | 
			
		||||
import { PerfTimer } from "../perf"
 | 
			
		||||
import {
 | 
			
		||||
  ComponentResources,
 | 
			
		||||
  emitComponentResources,
 | 
			
		||||
  getComponentResources,
 | 
			
		||||
  getStaticResourcesFromPlugins,
 | 
			
		||||
} from "../plugins"
 | 
			
		||||
import { getStaticResourcesFromPlugins } from "../plugins"
 | 
			
		||||
import { EmitCallback } from "../plugins/types"
 | 
			
		||||
import { ProcessedContent } from "../plugins/vfile"
 | 
			
		||||
import { FilePath } from "../path"
 | 
			
		||||
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import spaRouterScript from "../components/scripts/spa.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import plausibleScript from "../components/scripts/plausible.inline"
 | 
			
		||||
// @ts-ignore
 | 
			
		||||
import popoverScript from "../components/scripts/popover.inline"
 | 
			
		||||
import popoverStyle from "../components/styles/popover.scss"
 | 
			
		||||
import { StaticResources } from "../resources"
 | 
			
		||||
import { QuartzLogger } from "../log"
 | 
			
		||||
import { googleFontHref } from "../theme"
 | 
			
		||||
import { trace } from "../trace"
 | 
			
		||||
import { BuildCtx } from "../ctx"
 | 
			
		||||
 | 
			
		||||
function addGlobalPageResources(
 | 
			
		||||
  ctx: BuildCtx,
 | 
			
		||||
  staticResources: StaticResources,
 | 
			
		||||
  componentResources: ComponentResources,
 | 
			
		||||
) {
 | 
			
		||||
  const cfg = ctx.cfg.configuration
 | 
			
		||||
  const reloadScript = ctx.argv.serve
 | 
			
		||||
  staticResources.css.push(googleFontHref(cfg.theme))
 | 
			
		||||
 | 
			
		||||
  // popovers
 | 
			
		||||
  if (cfg.enablePopovers) {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(popoverScript)
 | 
			
		||||
    componentResources.css.push(popoverStyle)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (cfg.analytics?.provider === "google") {
 | 
			
		||||
    const tagId = cfg.analytics.tagId
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      src: `https://www.googletagmanager.com/gtag/js?id=${tagId}`,
 | 
			
		||||
      contentType: "external",
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
    })
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
    window.dataLayer = window.dataLayer || [];
 | 
			
		||||
    function gtag() { dataLayer.push(arguments); }
 | 
			
		||||
    gtag(\`js\`, new Date());
 | 
			
		||||
    gtag(\`config\`, \`${tagId}\`, { send_page_view: false });
 | 
			
		||||
 | 
			
		||||
    document.addEventListener(\`nav\`, () => {
 | 
			
		||||
      gtag(\`event\`, \`page_view\`, {
 | 
			
		||||
        page_title: document.title,
 | 
			
		||||
        page_location: location.href,
 | 
			
		||||
      });
 | 
			
		||||
    });`)
 | 
			
		||||
  } else if (cfg.analytics?.provider === "plausible") {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(plausibleScript)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // spa
 | 
			
		||||
  if (cfg.enableSPA) {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(spaRouterScript)
 | 
			
		||||
  } else {
 | 
			
		||||
    componentResources.afterDOMLoaded.push(`
 | 
			
		||||
      window.spaNavigate = (url, _) => window.location.assign(url)
 | 
			
		||||
      const event = new CustomEvent("nav", { detail: { slug: document.body.dataset.slug } })
 | 
			
		||||
      document.dispatchEvent(event)`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (reloadScript) {
 | 
			
		||||
    staticResources.js.push({
 | 
			
		||||
      loadTime: "afterDOMReady",
 | 
			
		||||
      contentType: "inline",
 | 
			
		||||
      script: `
 | 
			
		||||
        const socket = new WebSocket('ws://localhost:3001')
 | 
			
		||||
        socket.addEventListener('message', () => document.location.reload())
 | 
			
		||||
      `,
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
 | 
			
		||||
  const { argv, cfg } = ctx
 | 
			
		||||
  const perf = new PerfTimer()
 | 
			
		||||
| 
						 | 
				
			
			@ -98,27 +23,8 @@ export async function emitContent(ctx: BuildCtx, content: ProcessedContent[]) {
 | 
			
		|||
    return pathToPage
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // initialize from plugins
 | 
			
		||||
  const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
 | 
			
		||||
 | 
			
		||||
  // component specific scripts and styles
 | 
			
		||||
  const componentResources = getComponentResources(cfg.plugins)
 | 
			
		||||
 | 
			
		||||
  // important that this goes *after* component scripts
 | 
			
		||||
  // as the "nav" event gets triggered here and we should make sure
 | 
			
		||||
  // that everyone else had the chance to register a listener for it
 | 
			
		||||
  addGlobalPageResources(ctx, staticResources, componentResources)
 | 
			
		||||
 | 
			
		||||
  let emittedFiles = 0
 | 
			
		||||
  const emittedResources = await emitComponentResources(cfg.configuration, componentResources, emit)
 | 
			
		||||
  if (argv.verbose) {
 | 
			
		||||
    for (const file of emittedResources) {
 | 
			
		||||
      emittedFiles += 1
 | 
			
		||||
      console.log(`[emit:Resources] ${file}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // emitter plugins
 | 
			
		||||
  const staticResources = getStaticResourcesFromPlugins(cfg.plugins)
 | 
			
		||||
  for (const emitter of cfg.plugins.emitters) {
 | 
			
		||||
    try {
 | 
			
		||||
      const emitted = await emitter.emit(ctx, content, staticResources, emit)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue