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 { loadQuartzConfig } from "./quartz/plugins/loader/config-loader"
|
||||||
import * as Plugin from "./quartz/plugins"
|
export default await loadQuartzConfig()
|
||||||
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
|
|
||||||
|
|||||||
152
quartz.layout.ts
152
quartz.layout.ts
@ -1,150 +1,2 @@
|
|||||||
import { FullPageLayout } from "./quartz/cfg"
|
import { loadQuartzLayout } from "./quartz/plugins/loader/config-loader"
|
||||||
import * as Component from "./quartz/components"
|
export const layout = await loadQuartzLayout()
|
||||||
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],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|||||||
@ -25,7 +25,8 @@ class ComponentRegistry {
|
|||||||
source: string,
|
source: string,
|
||||||
manifest?: ComponentManifest,
|
manifest?: ComponentManifest,
|
||||||
): void {
|
): 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}`)
|
console.warn(`Component "${name}" is being overwritten by ${source}`)
|
||||||
}
|
}
|
||||||
this.components.set(name, { component, source, manifest })
|
this.components.set(name, { component, source, manifest })
|
||||||
@ -40,12 +41,27 @@ class ComponentRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAllComponents(): QuartzComponent[] {
|
getAllComponents(): QuartzComponent[] {
|
||||||
return Array.from(this.components.values()).map((r) => {
|
// Deduplicate by component reference (same constructor may be registered under multiple keys)
|
||||||
if (typeof r.component === "function") {
|
const seen = new Set<QuartzComponent | QuartzComponentConstructor>()
|
||||||
return (r.component as QuartzComponentConstructor)(undefined)
|
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 { componentRegistry } from "../../components/registry"
|
||||||
import { ComponentManifest, PluginManifest } from "./types"
|
import { ComponentManifest, PluginManifest } from "./types"
|
||||||
import { QuartzComponentConstructor } from "../../components/types"
|
import { QuartzComponentConstructor } from "../../components/types"
|
||||||
|
import { getPluginSubpathEntry } from "./gitLoader"
|
||||||
|
|
||||||
export async function loadComponentsFromPackage(
|
export async function loadComponentsFromPackage(
|
||||||
packageName: string,
|
pluginName: string,
|
||||||
manifest: PluginManifest | null,
|
manifest: PluginManifest | null,
|
||||||
|
subdir?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!manifest?.components) return
|
if (!manifest?.components) return
|
||||||
|
|
||||||
try {
|
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]
|
const component = componentsModule[exportName]
|
||||||
if (!component) {
|
if (!component) {
|
||||||
console.warn(
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullName = `${packageName}/${exportName}`
|
// Register under the fully-qualified key (pluginName/exportName)
|
||||||
componentRegistry.register(
|
componentRegistry.register(
|
||||||
fullName,
|
`${pluginName}/${exportName}`,
|
||||||
component as QuartzComponentConstructor,
|
component as QuartzComponentConstructor,
|
||||||
packageName,
|
pluginName,
|
||||||
componentManifest as ComponentManifest,
|
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 {
|
} catch {
|
||||||
// Components module doesn't exist, that's okay for plugins without components
|
|
||||||
if (manifest.components && Object.keys(manifest.components).length > 0) {
|
if (manifest.components && Object.keys(manifest.components).length > 0) {
|
||||||
console.warn(
|
console.warn(`Plugin "${pluginName}" declares components but failed to load them`)
|
||||||
`Plugin ${packageName} declares components but failed to load them from ${packageName}/components`,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
LayoutConfig,
|
LayoutConfig,
|
||||||
PluginLayoutDeclaration,
|
PluginLayoutDeclaration,
|
||||||
FlexGroupConfig,
|
FlexGroupConfig,
|
||||||
PluginCategory,
|
|
||||||
} from "./types"
|
} from "./types"
|
||||||
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader"
|
||||||
import { loadComponentsFromPackage } from "./componentLoader"
|
import { loadComponentsFromPackage } from "./componentLoader"
|
||||||
@ -284,8 +283,17 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
|||||||
pageTypes.push({ entry, manifest })
|
pageTypes.push({ entry, manifest })
|
||||||
break
|
break
|
||||||
default: {
|
default: {
|
||||||
// Try to detect category from the loaded module
|
|
||||||
const gitSpec = parsePluginSource(entry.source)
|
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)
|
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||||
try {
|
try {
|
||||||
const module = await import(entryPoint)
|
const module = await import(entryPoint)
|
||||||
@ -298,6 +306,8 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
|||||||
pageType: pageTypes,
|
pageType: pageTypes,
|
||||||
}[detected]
|
}[detected]
|
||||||
target.push({ entry, manifest })
|
target.push({ entry, manifest })
|
||||||
|
} else if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||||
|
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||||
} else {
|
} else {
|
||||||
console.warn(
|
console.warn(
|
||||||
styleText("yellow", `⚠`) +
|
styleText("yellow", `⚠`) +
|
||||||
@ -305,10 +315,14 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
console.warn(
|
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
||||||
styleText("yellow", `⚠`) +
|
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
|
||||||
` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
|
} 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
|
// Instantiate plugins
|
||||||
const instantiate = async (
|
const instantiate = async (
|
||||||
items: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[],
|
items: { entry: PluginJsonEntry; manifest: PluginManifest | undefined }[],
|
||||||
|
expectedCategory: ProcessingCategory,
|
||||||
) => {
|
) => {
|
||||||
const instances = []
|
const instances = []
|
||||||
for (const { entry, manifest } of items) {
|
for (const { entry, manifest } of items) {
|
||||||
@ -339,22 +354,18 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
|||||||
const gitSpec = parsePluginSource(entry.source)
|
const gitSpec = parsePluginSource(entry.source)
|
||||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
||||||
const module = await import(entryPoint)
|
const module = await import(entryPoint)
|
||||||
|
|
||||||
// Load components if declared
|
|
||||||
if (manifest?.components && Object.keys(manifest.components).length > 0) {
|
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
|
const factory = findFactory(module, expectedCategory)
|
||||||
if (typeof factory !== "function") {
|
if (!factory) {
|
||||||
console.warn(
|
console.warn(
|
||||||
styleText("yellow", `⚠`) +
|
styleText("yellow", `⚠`) +
|
||||||
` Plugin "${extractPluginName(entry.source)}" has no factory function. Skipping.`,
|
` Plugin "${extractPluginName(entry.source)}" has no factory function for category "${expectedCategory}". Skipping.`,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge default options with user options
|
|
||||||
const options = { ...manifest?.defaultOptions, ...entry.options }
|
const options = { ...manifest?.defaultOptions, ...entry.options }
|
||||||
instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
|
instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -378,19 +389,77 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
|
|||||||
const builtinPageTypes = [builtinPlugins.PageTypes.NotFoundPageType()]
|
const builtinPageTypes = [builtinPlugins.PageTypes.NotFoundPageType()]
|
||||||
|
|
||||||
const plugins: PluginTypes = {
|
const plugins: PluginTypes = {
|
||||||
transformers: [...builtinTransformers, ...(await instantiate(transformers))],
|
transformers: [...builtinTransformers, ...(await instantiate(transformers, "transformer"))],
|
||||||
filters: await instantiate(filters),
|
filters: await instantiate(filters, "filter"),
|
||||||
emitters: [...builtinEmitters, ...(await instantiate(emitters))],
|
emitters: [...builtinEmitters, ...(await instantiate(emitters, "emitter"))],
|
||||||
pageTypes: [...(await instantiate(pageTypes)), ...builtinPageTypes],
|
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 {
|
return {
|
||||||
configuration,
|
configuration,
|
||||||
plugins,
|
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
|
if (!module || typeof module !== "object") return null
|
||||||
const mod = module as Record<string, unknown>
|
const mod = module as Record<string, unknown>
|
||||||
|
|
||||||
@ -474,23 +543,24 @@ export async function loadQuartzLayout(): Promise<{
|
|||||||
const HeadModule = await import("../../components/Head")
|
const HeadModule = await import("../../components/Head")
|
||||||
const head = HeadModule.default()
|
const head = HeadModule.default()
|
||||||
|
|
||||||
// Find footer plugin
|
// Find footer from component registry (loaded during plugin instantiation)
|
||||||
const footerEntry = json.plugins.find(
|
const footerEntry = json.plugins.find(
|
||||||
(e) => e.enabled && extractPluginName(e.source) === "footer",
|
(e) => e.enabled && extractPluginName(e.source) === "footer",
|
||||||
)
|
)
|
||||||
let footer: QuartzComponent | undefined
|
let footer: QuartzComponent | undefined
|
||||||
if (footerEntry) {
|
if (footerEntry) {
|
||||||
try {
|
// Try registry lookup: plugin name ("footer") or export name ("Footer")
|
||||||
const gitSpec = parsePluginSource(footerEntry.source)
|
const footerReg = componentRegistry.get("footer") ?? componentRegistry.get("Footer")
|
||||||
const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir)
|
if (footerReg) {
|
||||||
const module = await import(entryPoint)
|
if (typeof footerReg.component === "function" && !("displayName" in footerReg.component)) {
|
||||||
const factory = module.default ?? module.plugin
|
// It's a constructor, instantiate with options
|
||||||
if (typeof factory === "function") {
|
const opts = { ...footerEntry.options }
|
||||||
const options = { ...footerEntry.options }
|
footer = (footerReg.component as Function)(
|
||||||
footer = factory(Object.keys(options).length > 0 ? options : undefined)
|
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 {
|
export function getPluginEntryPoint(name: string, subdir?: string): string {
|
||||||
const pluginDir = getPluginDir(name)
|
const pluginDir = getPluginDir(name)
|
||||||
const searchDir = subdir ? path.join(pluginDir, subdir) : pluginDir
|
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 = [
|
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.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) {
|
for (const candidate of candidates) {
|
||||||
if (fs.existsSync(candidate)) {
|
if (fs.existsSync(candidate)) {
|
||||||
return candidate
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no entry found, return the search dir and let Node handle it
|
// If no entry found, return the search dir and let Node handle it
|
||||||
return searchDir
|
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
|
* Update all installed plugins
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
} from "../types"
|
} from "../types"
|
||||||
import { BuildCtx } from "../../util/ctx"
|
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"
|
export type LayoutPosition = "left" | "right" | "beforeBody" | "afterBody"
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user