mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-05-18 14:34:23 +02:00

For the current setup where people have to fork or at least clone quartz this changes nothing - but it allows you to install quartz as a devDependency via npm and have it actually work. One real change is switch from `.quartz-cache` to `node_modules/.cache/quartz` for transpilation results, this is an artifact from my previous attempts, I guess with this one I can change it back - but `node_modules/.cache` feels more better imo idk. edit: OTOH if you want to have quartz be a _completely_ separate binary (which this also enables I think), having it create a node_modules folder is weird, so I made a quick hack for that for now. Example: ```bash $ mkdir my-repo && cd my-repo $ npm i quartz@necauqua/quartz#untangled # quartz@ prefix is important $ cp node_modules/quartz/quartz.*.ts . # copy the default configs $ mkdir content && echo "# Hello World!" > content/index.md $ npx quartz build --serve # this just works! $ echo 'body { background: red !important; }' > styles.scss ``` Notice how I used my branch in the `npm i` line, ideally it'd be `npm i quartz@jackyzho0/quartz`, or maybe we can somehow get the quartz package on npm and it'll just be `npm i quartz`. In the latter case `npx quartz build` will literally just work without a local npm package at all?. Having some support for components and plugins being in separate npm packages instead of people copying code around is not out of the picture with this too btw. Closes #502 MOVE ME
298 lines
12 KiB
TypeScript
298 lines
12 KiB
TypeScript
import { FilePath, FullSlug, joinSegments } from "../../util/path"
|
|
import { QuartzEmitterPlugin } from "../types"
|
|
|
|
// @ts-ignore
|
|
import spaRouterScript from "../../components/scripts/spa.inline"
|
|
// @ts-ignore
|
|
import popoverScript from "../../components/scripts/popover.inline"
|
|
import styles from "../../styles/custom.scss"
|
|
import popoverStyle from "../../components/styles/popover.scss"
|
|
import { BuildCtx } from "../../util/ctx"
|
|
import { QuartzComponent } from "../../components/types"
|
|
import { googleFontHref, joinStyles } from "../../util/theme"
|
|
import { Features, transform } from "lightningcss"
|
|
import { transform as transpile } from "esbuild"
|
|
import { write } from "./helpers"
|
|
import DepGraph from "../../depgraph"
|
|
import path from "path"
|
|
|
|
type ComponentResources = {
|
|
css: string[]
|
|
beforeDOMLoaded: string[]
|
|
afterDOMLoaded: string[]
|
|
}
|
|
|
|
function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|
const allComponents: Set<QuartzComponent> = new Set()
|
|
for (const emitter of ctx.cfg.plugins.emitters) {
|
|
const components = emitter.getQuartzComponents(ctx)
|
|
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],
|
|
}
|
|
}
|
|
|
|
async function joinScripts(scripts: string[]): Promise<string> {
|
|
// wrap with iife to prevent scope collision
|
|
const script = scripts.map((script) => `(function () {${script}})();`).join("\n")
|
|
|
|
// minify with esbuild
|
|
const res = await transpile(script, {
|
|
minify: true,
|
|
})
|
|
|
|
return res.code
|
|
}
|
|
|
|
function addGlobalPageResources(ctx: BuildCtx, componentResources: ComponentResources) {
|
|
const cfg = ctx.cfg.configuration
|
|
|
|
// popovers
|
|
if (cfg.enablePopovers) {
|
|
componentResources.afterDOMLoaded.push(popoverScript)
|
|
componentResources.css.push(popoverStyle)
|
|
}
|
|
|
|
if (cfg.analytics?.provider === "google") {
|
|
const tagId = cfg.analytics.tagId
|
|
componentResources.afterDOMLoaded.push(`
|
|
const gtagScript = document.createElement("script")
|
|
gtagScript.src = "https://www.googletagmanager.com/gtag/js?id=${tagId}"
|
|
gtagScript.async = true
|
|
document.head.appendChild(gtagScript)
|
|
|
|
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") {
|
|
const plausibleHost = cfg.analytics.host ?? "https://plausible.io"
|
|
componentResources.afterDOMLoaded.push(`
|
|
const plausibleScript = document.createElement("script")
|
|
plausibleScript.src = "${plausibleHost}/js/script.manual.js"
|
|
plausibleScript.setAttribute("data-domain", location.hostname)
|
|
plausibleScript.defer = true
|
|
document.head.appendChild(plausibleScript)
|
|
|
|
window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }
|
|
|
|
document.addEventListener("nav", () => {
|
|
plausible("pageview")
|
|
})
|
|
`)
|
|
} else if (cfg.analytics?.provider === "umami") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
const umamiScript = document.createElement("script")
|
|
umamiScript.src = "${cfg.analytics.host ?? "https://analytics.umami.is"}/script.js"
|
|
umamiScript.setAttribute("data-website-id", "${cfg.analytics.websiteId}")
|
|
umamiScript.async = true
|
|
|
|
document.head.appendChild(umamiScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "goatcounter") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
const goatcounterScript = document.createElement("script")
|
|
goatcounterScript.src = "${cfg.analytics.scriptSrc ?? "https://gc.zgo.at/count.js"}"
|
|
goatcounterScript.async = true
|
|
goatcounterScript.setAttribute("data-goatcounter",
|
|
"https://${cfg.analytics.websiteId}.${cfg.analytics.host ?? "goatcounter.com"}/count")
|
|
document.head.appendChild(goatcounterScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "posthog") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
const posthogScript = document.createElement("script")
|
|
posthogScript.innerHTML= \`!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys onSessionId".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
|
|
posthog.init('${cfg.analytics.apiKey}',{api_host:'${cfg.analytics.host ?? "https://app.posthog.com"}'})\`
|
|
document.head.appendChild(posthogScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "tinylytics") {
|
|
const siteId = cfg.analytics.siteId
|
|
componentResources.afterDOMLoaded.push(`
|
|
const tinylyticsScript = document.createElement("script")
|
|
tinylyticsScript.src = "https://tinylytics.app/embed/${siteId}.js"
|
|
tinylyticsScript.defer = true
|
|
document.head.appendChild(tinylyticsScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "cabin") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
const cabinScript = document.createElement("script")
|
|
cabinScript.src = "${cfg.analytics.host ?? "https://scripts.withcabin.com"}/hello.js"
|
|
cabinScript.defer = true
|
|
cabinScript.async = true
|
|
document.head.appendChild(cabinScript)
|
|
`)
|
|
} else if (cfg.analytics?.provider === "clarity") {
|
|
componentResources.afterDOMLoaded.push(`
|
|
const clarityScript = document.createElement("script")
|
|
clarityScript.innerHTML= \`(function(c,l,a,r,i,t,y){c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
|
|
t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
|
|
y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
|
|
})(window, document, "clarity", "script", "${cfg.analytics.projectId}");\`
|
|
document.head.appendChild(clarityScript)
|
|
`)
|
|
}
|
|
|
|
if (cfg.enableSPA) {
|
|
componentResources.afterDOMLoaded.push(spaRouterScript)
|
|
} else {
|
|
componentResources.afterDOMLoaded.push(`
|
|
window.spaNavigate = (url, _) => window.location.assign(url)
|
|
window.addCleanup = () => {}
|
|
const event = new CustomEvent("nav", { detail: { url: document.body.dataset.slug } })
|
|
document.dispatchEvent(event)
|
|
`)
|
|
}
|
|
}
|
|
|
|
// This emitter should not update the `resources` parameter. If it does, partial
|
|
// rebuilds may not work as expected.
|
|
export const ComponentResources: QuartzEmitterPlugin = () => {
|
|
return {
|
|
name: "ComponentResources",
|
|
getQuartzComponents() {
|
|
return []
|
|
},
|
|
async getDependencyGraph(ctx, _content, _resources) {
|
|
const graph = new DepGraph<FilePath>()
|
|
graph.addEdge(
|
|
path.join(ctx.argv.output, "index.css") as FilePath,
|
|
path.join(process.cwd(), "styles.scss") as FilePath,
|
|
)
|
|
return graph
|
|
},
|
|
async emit(ctx, _content, _resources): Promise<FilePath[]> {
|
|
const promises: Promise<FilePath>[] = []
|
|
const cfg = ctx.cfg.configuration
|
|
// component specific scripts and styles
|
|
const componentResources = getComponentResources(ctx)
|
|
let googleFontsStyleSheet = ""
|
|
if (cfg.theme.fontOrigin === "local") {
|
|
// let the user do it themselves in css
|
|
} else if (cfg.theme.fontOrigin === "googleFonts" && !cfg.theme.cdnCaching) {
|
|
// when cdnCaching is true, we link to google fonts in Head.tsx
|
|
let match
|
|
|
|
const fontSourceRegex = /url\((https:\/\/fonts.gstatic.com\/s\/[^)]+\.(woff2|ttf))\)/g
|
|
|
|
googleFontsStyleSheet = await (
|
|
await fetch(googleFontHref(ctx.cfg.configuration.theme))
|
|
).text()
|
|
|
|
while ((match = fontSourceRegex.exec(googleFontsStyleSheet)) !== null) {
|
|
// match[0] is the `url(path)`, match[1] is the `path`
|
|
const url = match[1]
|
|
// the static name of this file.
|
|
const [filename, ext] = url.split("/").pop()!.split(".")
|
|
|
|
googleFontsStyleSheet = googleFontsStyleSheet.replace(
|
|
url,
|
|
`https://${cfg.baseUrl}/static/fonts/${filename}.ttf`,
|
|
)
|
|
|
|
promises.push(
|
|
fetch(url)
|
|
.then((res) => {
|
|
if (!res.ok) {
|
|
throw new Error(`Failed to fetch font`)
|
|
}
|
|
return res.arrayBuffer()
|
|
})
|
|
.then((buf) =>
|
|
write({
|
|
ctx,
|
|
slug: joinSegments("static", "fonts", filename) as FullSlug,
|
|
ext: `.${ext}`,
|
|
content: Buffer.from(buf),
|
|
}),
|
|
),
|
|
)
|
|
}
|
|
}
|
|
|
|
// 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, componentResources)
|
|
|
|
const stylesheet = joinStyles(
|
|
ctx.cfg.configuration.theme,
|
|
googleFontsStyleSheet,
|
|
...componentResources.css,
|
|
styles,
|
|
await import("$styles").then((s) => s.default ?? s).catch(() => ""),
|
|
)
|
|
const [prescript, postscript] = await Promise.all([
|
|
joinScripts(componentResources.beforeDOMLoaded),
|
|
joinScripts(componentResources.afterDOMLoaded),
|
|
])
|
|
|
|
promises.push(
|
|
write({
|
|
ctx,
|
|
slug: "index" as FullSlug,
|
|
ext: ".css",
|
|
content: transform({
|
|
filename: "index.css",
|
|
code: Buffer.from(stylesheet),
|
|
minify: true,
|
|
targets: {
|
|
safari: (15 << 16) | (6 << 8), // 15.6
|
|
ios_saf: (15 << 16) | (6 << 8), // 15.6
|
|
edge: 115 << 16,
|
|
firefox: 102 << 16,
|
|
chrome: 109 << 16,
|
|
},
|
|
include: Features.MediaQueries,
|
|
}).code.toString(),
|
|
}),
|
|
write({
|
|
ctx,
|
|
slug: "prescript" as FullSlug,
|
|
ext: ".js",
|
|
content: prescript,
|
|
}),
|
|
write({
|
|
ctx,
|
|
slug: "postscript" as FullSlug,
|
|
ext: ".js",
|
|
content: postscript,
|
|
}),
|
|
)
|
|
|
|
return await Promise.all(promises)
|
|
},
|
|
}
|
|
}
|