diff --git a/quartz/components/frames/index.ts b/quartz/components/frames/index.ts index b772fdfba..b39d05697 100644 --- a/quartz/components/frames/index.ts +++ b/quartz/components/frames/index.ts @@ -2,11 +2,14 @@ import { PageFrame } from "./types" import { DefaultFrame } from "./DefaultFrame" import { FullWidthFrame } from "./FullWidthFrame" import { MinimalFrame } from "./MinimalFrame" +import { frameRegistry } from "./registry" export type { PageFrame, PageFrameProps } from "./types" export { DefaultFrame } from "./DefaultFrame" export { FullWidthFrame } from "./FullWidthFrame" export { MinimalFrame } from "./MinimalFrame" +export { frameRegistry } from "./registry" +export type { RegisteredFrame } from "./registry" /** * Registry of built-in page frames. Page types can reference these by name @@ -22,17 +25,29 @@ const builtinFrames: Record = { } /** - * Resolve a frame by name. Returns the DefaultFrame if the name is not found, - * logging a warning for unknown frame names. + * Resolve a frame by name. Checks plugin-registered frames first, + * then built-in frames, then falls back to DefaultFrame. */ export function resolveFrame(name: string | undefined): PageFrame { if (!name || name === "default") { return DefaultFrame } + + // Check plugin-registered frames first + const registered = frameRegistry.get(name) + if (registered) { + return registered.frame + } + + // Fall back to built-in frames const frame = builtinFrames[name] if (!frame) { + const allFrameNames = [ + ...Object.keys(builtinFrames), + ...[...frameRegistry.getAll().keys()], + ] console.warn( - `Unknown page frame "${name}", falling back to "default". Available frames: ${Object.keys(builtinFrames).join(", ")}`, + `Unknown page frame "${name}", falling back to "default". Available frames: ${allFrameNames.join(", ")}`, ) return DefaultFrame } diff --git a/quartz/components/frames/registry.ts b/quartz/components/frames/registry.ts new file mode 100644 index 000000000..12b867adf --- /dev/null +++ b/quartz/components/frames/registry.ts @@ -0,0 +1,34 @@ +import { PageFrame } from "./types" + +export interface RegisteredFrame { + frame: PageFrame + source: string +} + +class FrameRegistry { + private frames = new Map() + + register(name: string, frame: PageFrame, source: string): void { + const existing = this.frames.get(name) + if (existing && existing.source !== source) { + console.warn( + `Page frame "${name}" from ${source} is overwriting frame from ${existing.source}`, + ) + } + this.frames.set(name, { frame, source }) + } + + get(name: string): RegisteredFrame | undefined { + return this.frames.get(name) + } + + getAll(): Map { + return new Map(this.frames) + } + + has(name: string): boolean { + return this.frames.has(name) + } +} + +export const frameRegistry = new FrameRegistry() diff --git a/quartz/plugins/loader/config-loader.ts b/quartz/plugins/loader/config-loader.ts index 6e04a097e..a2c796f75 100644 --- a/quartz/plugins/loader/config-loader.ts +++ b/quartz/plugins/loader/config-loader.ts @@ -15,6 +15,7 @@ import { } from "./types" import { parsePluginSource, installPlugin, getPluginEntryPoint, toFileUrl } from "./gitLoader" import { loadComponentsFromPackage } from "./componentLoader" +import { loadFramesFromPackage } from "./frameLoader" import { componentRegistry } from "../../components/registry" import { getCondition } from "./conditions" @@ -190,6 +191,7 @@ async function readManifestFromPackageJson(source: string): Promise 0) { await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir) } + if (manifest?.frames && Object.keys(manifest.frames).length > 0) { + await loadFramesFromPackage(gitSpec.name, manifest, gitSpec.subdir) + } break } const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir) @@ -313,6 +318,9 @@ export async function loadQuartzConfig( target.push({ entry, manifest }) } else if (manifest?.components && Object.keys(manifest.components).length > 0) { await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir) + if (manifest?.frames && Object.keys(manifest.frames).length > 0) { + await loadFramesFromPackage(gitSpec.name, manifest, gitSpec.subdir) + } } else { console.warn( styleText("yellow", `⚠`) + @@ -320,9 +328,15 @@ export async function loadQuartzConfig( ) } } catch { - if (manifest?.components && Object.keys(manifest.components).length > 0) { + const hasComponents = manifest?.components && Object.keys(manifest.components).length > 0 + const hasFrames = manifest?.frames && Object.keys(manifest.frames).length > 0 + if (hasComponents) { await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir) - } else { + } + if (hasFrames) { + await loadFramesFromPackage(gitSpec.name, manifest, gitSpec.subdir) + } + if (!hasComponents && !hasFrames) { console.warn( styleText("yellow", `⚠`) + ` Could not load plugin "${extractPluginName(entry.source)}" to detect category. Skipping.`, @@ -362,6 +376,9 @@ export async function loadQuartzConfig( if (manifest?.components && Object.keys(manifest.components).length > 0) { await loadComponentsFromPackage(gitSpec.name, manifest, gitSpec.subdir) } + if (manifest?.frames && Object.keys(manifest.frames).length > 0) { + await loadFramesFromPackage(gitSpec.name, manifest, gitSpec.subdir) + } const factory = findFactory(module, expectedCategory) if (!factory) { diff --git a/quartz/plugins/loader/frameLoader.ts b/quartz/plugins/loader/frameLoader.ts new file mode 100644 index 000000000..746c824b5 --- /dev/null +++ b/quartz/plugins/loader/frameLoader.ts @@ -0,0 +1,48 @@ +import { frameRegistry } from "../../components/frames/registry" +import { PluginManifest } from "./types" +import { PageFrame } from "../../components/frames/types" +import { getPluginSubpathEntry, toFileUrl } from "./gitLoader" + +export async function loadFramesFromPackage( + pluginName: string, + manifest: PluginManifest | null, + subdir?: string, +): Promise { + if (!manifest?.frames) return + + try { + const framesPath = getPluginSubpathEntry(pluginName, "./frames", subdir) + + let framesModule: Record + if (framesPath) { + framesModule = await import(toFileUrl(framesPath)) + } else { + framesModule = await import(`${pluginName}/frames`) + } + + for (const [exportName, frameMeta] of Object.entries(manifest.frames)) { + const frame = framesModule[exportName] + if (!frame) { + console.warn( + `Frame "${exportName}" declared in manifest but not found in ${pluginName}/frames`, + ) + continue + } + + const pageFrame = frame as PageFrame + if (!pageFrame.name || typeof pageFrame.render !== "function") { + console.warn( + `Frame "${exportName}" from ${pluginName} is not a valid PageFrame (missing name or render)`, + ) + continue + } + + // Register under the frame's declared name + frameRegistry.register(pageFrame.name, pageFrame, pluginName) + } + } catch { + if (manifest.frames && Object.keys(manifest.frames).length > 0) { + console.warn(`Plugin "${pluginName}" declares frames but failed to load them`) + } + } +} diff --git a/quartz/plugins/loader/types.ts b/quartz/plugins/loader/types.ts index 0cb0432ec..9d456ef72 100644 --- a/quartz/plugins/loader/types.ts +++ b/quartz/plugins/loader/types.ts @@ -63,6 +63,8 @@ export interface PluginManifest { configSchema?: object /** Components provided by this plugin, keyed by component export name */ components?: Record + /** Page frames provided by this plugin, keyed by export name. Each entry maps to a PageFrame object. */ + frames?: Record } /**