fix: plugin mapping from configuration

This commit is contained in:
saberzero1 2026-02-23 23:47:50 +01:00
parent d872922f06
commit 0007ed3f29
No known key found for this signature in database
7 changed files with 250 additions and 357 deletions

View File

@ -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

View File

@ -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],
},
},
}

View File

@ -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)
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") { if (typeof r.component === "function") {
return (r.component as QuartzComponentConstructor)(undefined) instance = (r.component as QuartzComponentConstructor)(undefined)
} else {
instance = r.component as QuartzComponent
} }
return r.component as QuartzComponent if (instance) {
}) results.push(instance)
}
} catch {
// Skip components that fail to instantiate
}
}
return results
} }
} }

View File

@ -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,
)
// 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, componentManifest as ComponentManifest,
) )
} }
} catch { }
// Components module doesn't exist, that's okay for plugins without components
if (manifest.components && Object.keys(manifest.components).length > 0) { // If plugin has exactly one component, also register under just the plugin name
console.warn( // (e.g. "footer", "note-properties") for direct kebab-case lookup
`Plugin ${packageName} declares components but failed to load them from ${packageName}/components`, 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 {
if (manifest.components && Object.keys(manifest.components).length > 0) {
console.warn(`Plugin "${pluginName}" declares components but failed to load them`)
}
}
} }

View File

@ -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,6 +315,9 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
) )
} }
} catch { } catch {
if (manifest?.components && Object.keys(manifest.components).length > 0) {
await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir)
} else {
console.warn( console.warn(
styleText("yellow", ``) + styleText("yellow", ``) +
` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`, ` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`,
@ -313,6 +326,7 @@ export async function loadQuartzConfig(): Promise<QuartzConfig> {
} }
} }
} }
}
// Sort by order within each category // Sort by order within each category
const sortByOrder = ( const sortByOrder = (
@ -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
} }
} }

View File

@ -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
*/ */

View File

@ -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"