diff --git a/content/features/upcoming features.md b/content/features/upcoming features.md index 8e7711e23..5c57dba2e 100644 --- a/content/features/upcoming features.md +++ b/content/features/upcoming features.md @@ -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 diff --git a/quartz.config.ts b/quartz.config.ts index 0bbc18136..5378a8e5b 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -95,6 +95,7 @@ const config: QuartzConfig = { filters: [Plugin.RemoveDrafts()], emitters: [ Plugin.AliasRedirects(), + Plugin.ComponentResources(), Plugin.ContentPage({ ...sharedPageComponents, ...contentPageLayout, diff --git a/quartz/bootstrap-cli.mjs b/quartz/bootstrap-cli.mjs index 475e6244b..aa26ffc5c 100755 --- a/quartz/bootstrap-cli.mjs +++ b/quartz/bootstrap-cli.mjs @@ -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) diff --git a/quartz/plugins/emitters/componentResources.ts b/quartz/plugins/emitters/componentResources.ts new file mode 100644 index 000000000..99f9657e9 --- /dev/null +++ b/quartz/plugins/emitters/componentResources.ts @@ -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 = 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(), + beforeDOMLoaded: new Set(), + afterDOMLoaded: new Set(), + } + + 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 { + // 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 + }, +}) diff --git a/quartz/plugins/emitters/index.ts b/quartz/plugins/emitters/index.ts index bb18361ba..d1149f783 100644 --- a/quartz/plugins/emitters/index.ts +++ b/quartz/plugins/emitters/index.ts @@ -5,3 +5,4 @@ export { ContentIndex } from "./contentIndex" export { AliasRedirects } from "./aliases" export { Assets } from "./assets" export { Static } from "./static" +export { ComponentResources } from "./componentResources" \ No newline at end of file diff --git a/quartz/plugins/index.ts b/quartz/plugins/index.ts index a6231dbdb..a8208e3fa 100644 --- a/quartz/plugins/index.ts +++ b/quartz/plugins/index.ts @@ -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 = 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(), - beforeDOMLoaded: new Set(), - afterDOMLoaded: new Set(), - } - - 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 { - 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: [], diff --git a/quartz/processors/emit.ts b/quartz/processors/emit.ts index ea7fda9e9..570f50590 100644 --- a/quartz/processors/emit.ts +++ b/quartz/processors/emit.ts @@ -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)