mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat: add FrameRegistry for plugin-provided page frames
Plugins can now register custom page frames via their manifest's 'frames' field. Frames are loaded alongside components during plugin initialization and resolved by name at render time with fallback to built-in frames.
This commit is contained in:
parent
88147be600
commit
ad617ac4d6
@ -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<string, PageFrame> = {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
|
||||
34
quartz/components/frames/registry.ts
Normal file
34
quartz/components/frames/registry.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { PageFrame } from "./types"
|
||||
|
||||
export interface RegisteredFrame {
|
||||
frame: PageFrame
|
||||
source: string
|
||||
}
|
||||
|
||||
class FrameRegistry {
|
||||
private frames = new Map<string, RegisteredFrame>()
|
||||
|
||||
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<string, RegisteredFrame> {
|
||||
return new Map(this.frames)
|
||||
}
|
||||
|
||||
has(name: string): boolean {
|
||||
return this.frames.has(name)
|
||||
}
|
||||
}
|
||||
|
||||
export const frameRegistry = new FrameRegistry()
|
||||
@ -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<PluginManife
|
||||
defaultOptions: q.defaultOptions,
|
||||
configSchema: q.configSchema,
|
||||
components: q.components,
|
||||
frames: q.frames,
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
@ -297,6 +299,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)
|
||||
}
|
||||
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) {
|
||||
|
||||
48
quartz/plugins/loader/frameLoader.ts
Normal file
48
quartz/plugins/loader/frameLoader.ts
Normal file
@ -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<void> {
|
||||
if (!manifest?.frames) return
|
||||
|
||||
try {
|
||||
const framesPath = getPluginSubpathEntry(pluginName, "./frames", subdir)
|
||||
|
||||
let framesModule: Record<string, unknown>
|
||||
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`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -63,6 +63,8 @@ export interface PluginManifest {
|
||||
configSchema?: object
|
||||
/** Components provided by this plugin, keyed by component export name */
|
||||
components?: Record<string, ComponentManifest & ComponentLayoutDefaults>
|
||||
/** Page frames provided by this plugin, keyed by export name. Each entry maps to a PageFrame object. */
|
||||
frames?: Record<string, { exportName: string }>
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user