mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
fix: plugin mapping from configuration
This commit is contained in:
parent
d872922f06
commit
0007ed3f29
153
quartz.config.ts
153
quartz.config.ts
@ -1,151 +1,2 @@
|
||||
import { QuartzConfig } from "./quartz/cfg"
|
||||
import * as Plugin from "./quartz/plugins"
|
||||
import * as ExternalPlugin from "./.quartz/plugins"
|
||||
import { layout } from "./quartz.layout"
|
||||
|
||||
const config: QuartzConfig = {
|
||||
configuration: {
|
||||
pageTitle: "Quartz 5",
|
||||
pageTitleSuffix: "",
|
||||
enableSPA: true,
|
||||
enablePopovers: true,
|
||||
analytics: {
|
||||
provider: "plausible",
|
||||
},
|
||||
locale: "en-US",
|
||||
baseUrl: "quartz.jzhao.xyz",
|
||||
ignorePatterns: ["private", "templates", ".obsidian"],
|
||||
defaultDateType: "modified",
|
||||
theme: {
|
||||
fontOrigin: "googleFonts",
|
||||
cdnCaching: true,
|
||||
typography: {
|
||||
header: "Schibsted Grotesk",
|
||||
body: "Source Sans Pro",
|
||||
code: "IBM Plex Mono",
|
||||
},
|
||||
colors: {
|
||||
lightMode: {
|
||||
light: "#faf8f8",
|
||||
lightgray: "#e5e5e5",
|
||||
gray: "#b8b8b8",
|
||||
darkgray: "#4e4e4e",
|
||||
dark: "#2b2b2b",
|
||||
secondary: "#284b63",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#fff23688",
|
||||
},
|
||||
darkMode: {
|
||||
light: "#161618",
|
||||
lightgray: "#393639",
|
||||
gray: "#646464",
|
||||
darkgray: "#d4d4d4",
|
||||
dark: "#ebebec",
|
||||
secondary: "#7b97aa",
|
||||
tertiary: "#84a59d",
|
||||
highlight: "rgba(143, 159, 169, 0.15)",
|
||||
textHighlight: "#b3aa0288",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
transformers: [
|
||||
ExternalPlugin.NoteProperties({
|
||||
includeAll: false,
|
||||
includedProperties: ["description", "tags", "aliases"],
|
||||
excludedProperties: [],
|
||||
hidePropertiesView: false,
|
||||
delimiters: "---",
|
||||
language: "yaml",
|
||||
}),
|
||||
ExternalPlugin.CreatedModifiedDate({
|
||||
priority: ["frontmatter", "git", "filesystem"],
|
||||
}),
|
||||
ExternalPlugin.SyntaxHighlighting({
|
||||
theme: {
|
||||
light: "github-light",
|
||||
dark: "github-dark",
|
||||
},
|
||||
keepBackground: false,
|
||||
}),
|
||||
ExternalPlugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false, enableCheckbox: true }),
|
||||
ExternalPlugin.GitHubFlavoredMarkdown(),
|
||||
ExternalPlugin.TableOfContentsTransformer(),
|
||||
ExternalPlugin.CrawlLinks({ markdownLinkResolution: "shortest" }),
|
||||
ExternalPlugin.Description(),
|
||||
ExternalPlugin.Latex({ renderEngine: "katex" }),
|
||||
],
|
||||
filters: [ExternalPlugin.RemoveDrafts()],
|
||||
emitters: [
|
||||
ExternalPlugin.AliasRedirects(),
|
||||
Plugin.ComponentResources(),
|
||||
ExternalPlugin.ContentIndex({
|
||||
enableSiteMap: true,
|
||||
enableRSS: true,
|
||||
}),
|
||||
Plugin.Assets(),
|
||||
Plugin.Static(),
|
||||
ExternalPlugin.Favicon(),
|
||||
Plugin.PageTypes.PageTypeDispatcher({
|
||||
defaults: layout.defaults,
|
||||
byPageType: layout.byPageType,
|
||||
}),
|
||||
ExternalPlugin.CustomOgImages(),
|
||||
ExternalPlugin.CNAME(),
|
||||
],
|
||||
pageTypes: [
|
||||
ExternalPlugin.CanvasPage(),
|
||||
ExternalPlugin.BasesPage(),
|
||||
ExternalPlugin.ContentPage(),
|
||||
ExternalPlugin.FolderPage(),
|
||||
ExternalPlugin.TagPage(),
|
||||
Plugin.PageTypes.NotFoundPageType(),
|
||||
],
|
||||
},
|
||||
externalPlugins: [
|
||||
"github:quartz-community/explorer",
|
||||
"github:quartz-community/graph",
|
||||
"github:quartz-community/search",
|
||||
"github:quartz-community/table-of-contents",
|
||||
"github:quartz-community/backlinks",
|
||||
"github:quartz-community/comments",
|
||||
"github:quartz-community/article-title",
|
||||
"github:quartz-community/tag-list",
|
||||
"github:quartz-community/page-title",
|
||||
"github:quartz-community/darkmode",
|
||||
"github:quartz-community/reader-mode",
|
||||
"github:quartz-community/content-meta",
|
||||
"github:quartz-community/footer",
|
||||
"github:quartz-community/content-page",
|
||||
"github:quartz-community/folder-page",
|
||||
"github:quartz-community/tag-page",
|
||||
"github:quartz-community/latex",
|
||||
"github:quartz-community/created-modified-date",
|
||||
"github:quartz-community/syntax-highlighting",
|
||||
"github:quartz-community/obsidian-flavored-markdown",
|
||||
"github:quartz-community/github-flavored-markdown",
|
||||
"github:quartz-community/crawl-links",
|
||||
"github:quartz-community/description",
|
||||
"github:quartz-community/hard-line-breaks",
|
||||
"github:quartz-community/citations",
|
||||
"github:quartz-community/ox-hugo",
|
||||
"github:quartz-community/roam",
|
||||
"github:quartz-community/remove-draft",
|
||||
"github:quartz-community/explicit-publish",
|
||||
"github:quartz-community/alias-redirects",
|
||||
"github:quartz-community/cname",
|
||||
"github:quartz-community/favicon",
|
||||
"github:quartz-community/content-index",
|
||||
"github:quartz-community/og-image",
|
||||
"github:quartz-community/canvas-page",
|
||||
"github:quartz-community/note-properties",
|
||||
"github:quartz-community/bases-page",
|
||||
"github:quartz-community/breadcrumbs",
|
||||
"github:quartz-community/spacer",
|
||||
"github:quartz-community/recent-notes",
|
||||
],
|
||||
}
|
||||
|
||||
export default config
|
||||
import { loadQuartzConfig } from "./quartz/plugins/loader/config-loader"
|
||||
export default await loadQuartzConfig()
|
||||
|
||||
152
quartz.layout.ts
152
quartz.layout.ts
@ -1,150 +1,2 @@
|
||||
import { FullPageLayout } from "./quartz/cfg"
|
||||
import * as Component from "./quartz/components"
|
||||
import * as Plugin from "./.quartz/plugins"
|
||||
|
||||
// Create plugin components once and reuse across layouts
|
||||
const explorerComponent = Plugin.Explorer()
|
||||
const graphComponent = Plugin.Graph()
|
||||
const searchComponent = Plugin.Search()
|
||||
const backlinksComponent = Plugin.Backlinks()
|
||||
const tocComponent = Plugin.TableOfContents()
|
||||
const articleTitleComponent = Plugin.ArticleTitle()
|
||||
const contentMetaComponent = Plugin.ContentMeta()
|
||||
const tagListComponent = Plugin.TagList()
|
||||
const pageTitleComponent = Plugin.PageTitle()
|
||||
const darkmodeComponent = Plugin.Darkmode()
|
||||
const readerModeComponent = Plugin.ReaderMode()
|
||||
const breadcrumbsComponent = Plugin.Breadcrumbs()
|
||||
const notePropertiesComponent = Plugin.NotePropertiesComponent()
|
||||
|
||||
export const layout: {
|
||||
defaults: Partial<FullPageLayout>
|
||||
byPageType: Record<string, Partial<FullPageLayout>>
|
||||
} = {
|
||||
// Components shared across all page types (can be overridden per page type)
|
||||
defaults: {
|
||||
head: Component.Head(),
|
||||
header: [],
|
||||
afterBody: [
|
||||
// Plugin.Comments({
|
||||
// provider: "giscus",
|
||||
// options: {}}),
|
||||
],
|
||||
footer: Plugin.Footer({
|
||||
links: {
|
||||
GitHub: "https://github.com/jackyzha0/quartz",
|
||||
"Discord Community": "https://discord.gg/cRFFHYye7t",
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
// Per-page-type layout overrides
|
||||
byPageType: {
|
||||
// Content pages (single notes)
|
||||
content: {
|
||||
beforeBody: [
|
||||
Component.ConditionalRender({
|
||||
component: breadcrumbsComponent,
|
||||
condition: (page) => page.fileData.slug !== "index",
|
||||
}),
|
||||
articleTitleComponent,
|
||||
notePropertiesComponent,
|
||||
contentMetaComponent,
|
||||
tagListComponent,
|
||||
],
|
||||
left: [
|
||||
pageTitleComponent,
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Flex({
|
||||
components: [
|
||||
{
|
||||
Component: searchComponent,
|
||||
grow: true,
|
||||
},
|
||||
{ Component: darkmodeComponent },
|
||||
{ Component: readerModeComponent },
|
||||
],
|
||||
}),
|
||||
explorerComponent,
|
||||
],
|
||||
right: [graphComponent, Component.DesktopOnly(tocComponent), backlinksComponent],
|
||||
},
|
||||
|
||||
// Folder listing pages
|
||||
folder: {
|
||||
beforeBody: [
|
||||
breadcrumbsComponent,
|
||||
articleTitleComponent,
|
||||
notePropertiesComponent,
|
||||
contentMetaComponent,
|
||||
],
|
||||
left: [
|
||||
pageTitleComponent,
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Flex({
|
||||
components: [
|
||||
{
|
||||
Component: searchComponent,
|
||||
grow: true,
|
||||
},
|
||||
{ Component: darkmodeComponent },
|
||||
],
|
||||
}),
|
||||
explorerComponent,
|
||||
],
|
||||
right: [],
|
||||
},
|
||||
|
||||
// Tag listing pages
|
||||
tag: {
|
||||
beforeBody: [
|
||||
breadcrumbsComponent,
|
||||
articleTitleComponent,
|
||||
notePropertiesComponent,
|
||||
contentMetaComponent,
|
||||
],
|
||||
left: [
|
||||
pageTitleComponent,
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Flex({
|
||||
components: [
|
||||
{
|
||||
Component: searchComponent,
|
||||
grow: true,
|
||||
},
|
||||
{ Component: darkmodeComponent },
|
||||
],
|
||||
}),
|
||||
explorerComponent,
|
||||
],
|
||||
right: [],
|
||||
},
|
||||
|
||||
// 404 page — minimal layout
|
||||
"404": {
|
||||
beforeBody: [],
|
||||
left: [],
|
||||
right: [],
|
||||
},
|
||||
|
||||
// Canvas pages — expansive layout, no sidebars
|
||||
canvas: {
|
||||
beforeBody: [breadcrumbsComponent, articleTitleComponent, notePropertiesComponent],
|
||||
left: [
|
||||
pageTitleComponent,
|
||||
Component.MobileOnly(Component.Spacer()),
|
||||
Component.Flex({
|
||||
components: [
|
||||
{
|
||||
Component: searchComponent,
|
||||
grow: true,
|
||||
},
|
||||
{ Component: darkmodeComponent },
|
||||
],
|
||||
}),
|
||||
explorerComponent,
|
||||
],
|
||||
right: [graphComponent, Component.DesktopOnly(tocComponent), backlinksComponent],
|
||||
},
|
||||
},
|
||||
}
|
||||
import { loadQuartzLayout } from "./quartz/plugins/loader/config-loader"
|
||||
export const layout = await loadQuartzLayout()
|
||||
|
||||
@ -25,7 +25,8 @@ class ComponentRegistry {
|
||||
source: string,
|
||||
manifest?: ComponentManifest,
|
||||
): void {
|
||||
if (this.components.has(name)) {
|
||||
const existing = this.components.get(name)
|
||||
if (existing && existing.source !== source) {
|
||||
console.warn(`Component "${name}" is being overwritten by ${source}`)
|
||||
}
|
||||
this.components.set(name, { component, source, manifest })
|
||||
@ -40,12 +41,27 @@ class ComponentRegistry {
|
||||
}
|
||||
|
||||
getAllComponents(): QuartzComponent[] {
|
||||
return Array.from(this.components.values()).map((r) => {
|
||||
if (typeof r.component === "function") {
|
||||
return (r.component as QuartzComponentConstructor)(undefined)
|
||||
// Deduplicate by component reference (same constructor may be registered under multiple keys)
|
||||
const seen = new Set<QuartzComponent | QuartzComponentConstructor>()
|
||||
const results: QuartzComponent[] = []
|
||||
for (const r of this.components.values()) {
|
||||
if (seen.has(r.component)) continue
|
||||
seen.add(r.component)
|
||||
try {
|
||||
let instance: QuartzComponent
|
||||
if (typeof r.component === "function") {
|
||||
instance = (r.component as QuartzComponentConstructor)(undefined)
|
||||
} else {
|
||||
instance = r.component as QuartzComponent
|
||||
}
|
||||
if (instance) {
|
||||
results.push(instance)
|
||||
}
|
||||
} catch {
|
||||
// Skip components that fail to instantiate
|
||||
}
|
||||
return r.component as QuartzComponent
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,39 +1,72 @@
|
||||
import { componentRegistry } from "../../components/registry"
|
||||
import { ComponentManifest, PluginManifest } from "./types"
|
||||
import { QuartzComponentConstructor } from "../../components/types"
|
||||
import { getPluginSubpathEntry } from "./gitLoader"
|
||||
|
||||
export async function loadComponentsFromPackage(
|
||||
packageName: string,
|
||||
pluginName: string,
|
||||
manifest: PluginManifest | null,
|
||||
subdir?: string,
|
||||
): Promise<void> {
|
||||
if (!manifest?.components) return
|
||||
|
||||
try {
|
||||
const componentsModule = await import(`${packageName}/components`)
|
||||
const componentsPath = getPluginSubpathEntry(pluginName, "./components", subdir)
|
||||
|
||||
for (const [exportName, componentManifest] of Object.entries(manifest.components)) {
|
||||
let componentsModule: Record<string, unknown>
|
||||
if (componentsPath) {
|
||||
componentsModule = await import(componentsPath)
|
||||
} else {
|
||||
componentsModule = await import(`${pluginName}/components`)
|
||||
}
|
||||
|
||||
const componentEntries = Object.entries(manifest.components)
|
||||
for (const [exportName, componentManifest] of componentEntries) {
|
||||
const component = componentsModule[exportName]
|
||||
if (!component) {
|
||||
console.warn(
|
||||
`Component "${exportName}" declared in manifest but not found in ${packageName}/components`,
|
||||
`Component "${exportName}" declared in manifest but not found in ${pluginName}/components`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const fullName = `${packageName}/${exportName}`
|
||||
// Register under the fully-qualified key (pluginName/exportName)
|
||||
componentRegistry.register(
|
||||
fullName,
|
||||
`${pluginName}/${exportName}`,
|
||||
component as QuartzComponentConstructor,
|
||||
packageName,
|
||||
pluginName,
|
||||
componentManifest as ComponentManifest,
|
||||
)
|
||||
|
||||
// Also register under just the export name (e.g. "Footer", "NotePropertiesComponent")
|
||||
// so buildLayoutForEntries can find it via PascalCase conversion of plugin name
|
||||
if (!componentRegistry.get(exportName)) {
|
||||
componentRegistry.register(
|
||||
exportName,
|
||||
component as QuartzComponentConstructor,
|
||||
pluginName,
|
||||
componentManifest as ComponentManifest,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// If plugin has exactly one component, also register under just the plugin name
|
||||
// (e.g. "footer", "note-properties") for direct kebab-case lookup
|
||||
if (componentEntries.length === 1) {
|
||||
const [exportName] = componentEntries[0]
|
||||
const component = componentsModule[exportName]
|
||||
if (component && !componentRegistry.get(pluginName)) {
|
||||
componentRegistry.register(
|
||||
pluginName,
|
||||
component as QuartzComponentConstructor,
|
||||
pluginName,
|
||||
componentEntries[0][1] as ComponentManifest,
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Components module doesn't exist, that's okay for plugins without components
|
||||
if (manifest.components && Object.keys(manifest.components).length > 0) {
|
||||
console.warn(
|
||||
`Plugin ${packageName} declares components but failed to load them from ${packageName}/components`,
|
||||
)
|
||||
console.warn(`Plugin "${pluginName}" declares components but failed to load them`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,6 @@ import {
|
||||
LayoutConfig,
|
||||
PluginLayoutDeclaration,
|
||||
FlexGroupConfig,
|
||||
PluginCategory,
|
||||
} from "./types"
|
||||
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
||||
import { loadComponentsFromPackage } from "./componentLoader"
|
||||
@ -284,8 +283,17 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
pageTypes.push({ entry, manifest })
|
||||
break
|
||||
default: {
|
||||
// Try to detect category from the loaded module
|
||||
const gitSpec = parsePluginSource(entry.source)
|
||||
const isComponentOnly =
|
||||
resolvedCategory === "component" ||
|
||||
(Array.isArray(category) && category.every((c) => c === "component"))
|
||||
|
||||
if (isComponentOnly) {
|
||||
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||
}
|
||||
break
|
||||
}
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
try {
|
||||
const module = await import(entryPoint)
|
||||
@ -298,6 +306,8 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
pageType: pageTypes,
|
||||
}[detected]
|
||||
target.push({ entry, manifest })
|
||||
} else if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||
} else {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
@ -305,10 +315,14 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
|
||||
)
|
||||
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||
} else {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -332,6 +346,7 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
// Instantiate plugins
|
||||
const instantiate = async (
|
||||
items: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[],
|
||||
expectedCategory: ProcessingCategory,
|
||||
) => {
|
||||
const instances = []
|
||||
for (const { entry, manifest } of items) {
|
||||
@ -339,22 +354,18 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
const gitSpec = parsePluginSource(entry.source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
const module = await import(entryPoint)
|
||||
|
||||
// Load components if declared
|
||||
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||
await loadComponentsFromPackage(entryPoint, manifest)
|
||||
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||
}
|
||||
|
||||
const factory = module.default ?? module.plugin
|
||||
if (typeof factory !== "function") {
|
||||
const factory = findFactory(module, expectedCategory)
|
||||
if (!factory) {
|
||||
console.warn(
|
||||
styleText("yellow", `⚠`) +
|
||||
` Plugin "${extractPluginName(entry.source)}" has no factory function. Skipping.`,
|
||||
` Plugin "${extractPluginName(entry.source)}" has no factory function for category "${expectedCategory}". Skipping.`,
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
// Merge default options with user options
|
||||
const options = { ...manifest?.defaultOptions, ...entry.options }
|
||||
instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
|
||||
} catch (err) {
|
||||
@ -378,19 +389,77 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
||||
const builtinPageTypes = [builtinPlugins.PageTypes.NotFoundPageType()]
|
||||
|
||||
const plugins: PluginTypes = {
|
||||
transformers: [...builtinTransformers, ...(await instantiate(transformers))],
|
||||
filters: await instantiate(filters),
|
||||
emitters: [...builtinEmitters, ...(await instantiate(emitters))],
|
||||
pageTypes: [...(await instantiate(pageTypes)), ...builtinPageTypes],
|
||||
transformers: [...builtinTransformers, ...(await instantiate(transformers, "transformer"))],
|
||||
filters: await instantiate(filters, "filter"),
|
||||
emitters: [...builtinEmitters, ...(await instantiate(emitters, "emitter"))],
|
||||
pageTypes: [...(await instantiate(pageTypes, "pageType")), ...builtinPageTypes],
|
||||
}
|
||||
|
||||
// Load layout and add PageTypeDispatcher to emitters.
|
||||
// This must happen after plugin instantiation so the component registry is populated.
|
||||
const layout = await loadQuartzLayout()
|
||||
plugins.emitters.push(
|
||||
builtinPlugins.PageTypes.PageTypeDispatcher({
|
||||
defaults: layout.defaults,
|
||||
byPageType: layout.byPageType,
|
||||
}),
|
||||
)
|
||||
return {
|
||||
configuration,
|
||||
plugins,
|
||||
}
|
||||
}
|
||||
|
||||
function detectCategoryFromModule(module: unknown): PluginCategory | null {
|
||||
type ProcessingCategory = "transformer" | "filter" | "emitter" | "pageType"
|
||||
|
||||
function matchesCategory(factory: Function, expected: ProcessingCategory): boolean {
|
||||
try {
|
||||
const instance = factory()
|
||||
if (!instance || typeof instance !== "object") return false
|
||||
switch (expected) {
|
||||
case "pageType":
|
||||
return "match" in instance && "body" in instance && "layout" in instance
|
||||
case "emitter":
|
||||
return "emit" in instance
|
||||
case "filter":
|
||||
return "shouldPublish" in instance
|
||||
case "transformer":
|
||||
return (
|
||||
"textTransform" in instance || "markdownPlugins" in instance || "htmlPlugins" in instance
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function findFactory(
|
||||
module: Record<string, unknown>,
|
||||
expectedCategory: ProcessingCategory,
|
||||
): Function | null {
|
||||
if (
|
||||
typeof module.default === "function" &&
|
||||
matchesCategory(module.default as Function, expectedCategory)
|
||||
) {
|
||||
return module.default as Function
|
||||
}
|
||||
if (
|
||||
typeof module.plugin === "function" &&
|
||||
matchesCategory(module.plugin as Function, expectedCategory)
|
||||
) {
|
||||
return module.plugin as Function
|
||||
}
|
||||
|
||||
for (const [, value] of Object.entries(module)) {
|
||||
if (typeof value === "function" && matchesCategory(value as Function, expectedCategory)) {
|
||||
return value as Function
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function detectCategoryFromModule(module: unknown): ProcessingCategory | null {
|
||||
if (!module || typeof module !== "object") return null
|
||||
const mod = module as Record<string, unknown>
|
||||
|
||||
@ -474,23 +543,24 @@ export async function loadQuartzLayout(): Promise<{
|
||||
const HeadModule = await import("../../components/Head")
|
||||
const head = HeadModule.default()
|
||||
|
||||
// Find footer plugin
|
||||
// Find footer from component registry (loaded during plugin instantiation)
|
||||
const footerEntry = json.plugins.find(
|
||||
(e) => e.enabled && extractPluginName(e.source) === "footer",
|
||||
)
|
||||
let footer: QuartzComponent | undefined
|
||||
if (footerEntry) {
|
||||
try {
|
||||
const gitSpec = parsePluginSource(footerEntry.source)
|
||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||
const module = await import(entryPoint)
|
||||
const factory = module.default ?? module.plugin
|
||||
if (typeof factory === "function") {
|
||||
const options = { ...footerEntry.options }
|
||||
footer = factory(Object.keys(options).length > 0 ? options : undefined)
|
||||
// Try registry lookup: plugin name ("footer") or export name ("Footer")
|
||||
const footerReg = componentRegistry.get("footer") ?? componentRegistry.get("Footer")
|
||||
if (footerReg) {
|
||||
if (typeof footerReg.component === "function" && !("displayName" in footerReg.component)) {
|
||||
// It's a constructor, instantiate with options
|
||||
const opts = { ...footerEntry.options }
|
||||
footer = (footerReg.component as Function)(
|
||||
Object.keys(opts).length > 0 ? opts : undefined,
|
||||
) as QuartzComponent
|
||||
} else {
|
||||
footer = footerReg.component as QuartzComponent
|
||||
}
|
||||
} catch {
|
||||
// Footer not available
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -168,31 +168,102 @@ export function isPluginInstalled(name: string): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the entry point for a plugin
|
||||
* Get the entry point for a plugin.
|
||||
* Prefers compiled dist/ output over raw src/ to avoid ESM resolution issues.
|
||||
*/
|
||||
export function getPluginEntryPoint(name: string, subdir?: string): string {
|
||||
const pluginDir = getPluginDir(name)
|
||||
const searchDir = subdir ? path.join(pluginDir, subdir) : pluginDir
|
||||
// Check package.json exports first (most reliable)
|
||||
const pkgJsonPath = path.join(searchDir, "package.json")
|
||||
if (fs.existsSync(pkgJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"))
|
||||
const exportEntry = pkg.exports?.["."]
|
||||
const importPath = typeof exportEntry === "string" ? exportEntry : exportEntry?.import
|
||||
if (importPath) {
|
||||
const resolved = path.join(searchDir, importPath)
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
// Fall back to main/module fields
|
||||
const mainField = pkg.module ?? pkg.main
|
||||
if (mainField) {
|
||||
const resolved = path.join(searchDir, mainField)
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// package.json parse error, fall through to candidates
|
||||
}
|
||||
}
|
||||
|
||||
// Try common entry points
|
||||
// Try common entry points — prefer compiled dist/ over raw src/
|
||||
const candidates = [
|
||||
path.join(searchDir, "src", "index.ts"),
|
||||
path.join(searchDir, "src", "index.js"),
|
||||
path.join(searchDir, "index.ts"),
|
||||
path.join(searchDir, "index.js"),
|
||||
path.join(searchDir, "dist", "index.js"),
|
||||
path.join(searchDir, "dist", "index.mjs"),
|
||||
path.join(searchDir, "index.js"),
|
||||
path.join(searchDir, "index.ts"),
|
||||
path.join(searchDir, "src", "index.js"),
|
||||
path.join(searchDir, "src", "index.ts"),
|
||||
]
|
||||
|
||||
for (const candidate of candidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
// If no entry found, return the search dir and let Node handle it
|
||||
return searchDir
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a subpath export for a plugin (e.g. "./components").
|
||||
* Uses package.json exports map, then falls back to dist/ directory structure.
|
||||
*/
|
||||
export function getPluginSubpathEntry(
|
||||
name: string,
|
||||
subpath: string,
|
||||
subdir?: string,
|
||||
): string | null {
|
||||
const pluginDir = getPluginDir(name)
|
||||
const searchDir = subdir ? path.join(pluginDir, subdir) : pluginDir
|
||||
|
||||
// Check package.json exports map
|
||||
const pkgJsonPath = path.join(searchDir, "package.json")
|
||||
if (fs.existsSync(pkgJsonPath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"))
|
||||
const exportEntry = pkg.exports?.[subpath]
|
||||
const importPath = typeof exportEntry === "string" ? exportEntry : exportEntry?.import
|
||||
if (importPath) {
|
||||
const resolved = path.join(searchDir, importPath)
|
||||
if (fs.existsSync(resolved)) {
|
||||
return resolved
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back: try dist/<subpath>/index.js
|
||||
const subpathClean = subpath.replace(/^\.\/?/, "")
|
||||
const fallbackCandidates = [
|
||||
path.join(searchDir, "dist", subpathClean, "index.js"),
|
||||
path.join(searchDir, "dist", `${subpathClean}.js`),
|
||||
path.join(searchDir, subpathClean, "index.js"),
|
||||
]
|
||||
|
||||
for (const candidate of fallbackCandidates) {
|
||||
if (fs.existsSync(candidate)) {
|
||||
return candidate
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
/**
|
||||
* Update all installed plugins
|
||||
*/
|
||||
|
||||
@ -6,7 +6,7 @@ import {
|
||||
} from "../types"
|
||||
import { BuildCtx } from "../../util/ctx"
|
||||
|
||||
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType"
|
||||
export type PluginCategory = "transformer" | "filter" | "emitter" | "pageType" | "component"
|
||||
|
||||
export type LayoutPosition = "left" | "right" | "beforeBody" | "afterBody"
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user