fix: improved plugin validation on install
This commit is contained in:
@@ -437,13 +437,30 @@ export async function loadQuartzConfig(
|
|||||||
if (!factory) {
|
if (!factory) {
|
||||||
console.warn(
|
console.warn(
|
||||||
styleText("yellow", `⚠`) +
|
styleText("yellow", `⚠`) +
|
||||||
` Plugin "${extractPluginName(entry.source)}" has no factory function for category "${expectedCategory}". Skipping.`,
|
` Plugin "${extractPluginName(entry.source)}" has no factory function for category "${expectedCategory}". ` +
|
||||||
|
`Ensure your plugin exports a default function, a "plugin" named export, or a single exported function.`,
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const pluginOverrides = componentRegistry.getOptionOverrides(gitSpec.name)
|
const pluginOverrides = componentRegistry.getOptionOverrides(gitSpec.name)
|
||||||
const options = { ...manifest?.defaultOptions, ...entry.options, ...pluginOverrides }
|
const options = { ...manifest?.defaultOptions, ...entry.options, ...pluginOverrides }
|
||||||
instances.push(factory(Object.keys(options).length > 0 ? options : undefined))
|
const instance = factory(Object.keys(options).length > 0 ? options : undefined)
|
||||||
|
if (!instance || typeof instance !== "object") {
|
||||||
|
console.warn(
|
||||||
|
styleText("yellow", `⚠`) +
|
||||||
|
` Plugin "${extractPluginName(entry.source)}" factory did not return a valid plugin instance. Skipping.`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (!validateCategory(instance, expectedCategory)) {
|
||||||
|
console.warn(
|
||||||
|
styleText("yellow", `⚠`) +
|
||||||
|
` Plugin "${extractPluginName(entry.source)}" declares category "${expectedCategory}" ` +
|
||||||
|
`but its factory returned an instance missing the required methods. Skipping.`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
instances.push(instance)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
styleText("red", `✗`) +
|
styleText("red", `✗`) +
|
||||||
@@ -488,47 +505,70 @@ export async function loadQuartzConfig(
|
|||||||
|
|
||||||
type ProcessingCategory = "transformer" | "filter" | "emitter" | "pageType"
|
type ProcessingCategory = "transformer" | "filter" | "emitter" | "pageType"
|
||||||
|
|
||||||
function matchesCategory(factory: Function, expected: ProcessingCategory): boolean {
|
/**
|
||||||
try {
|
* Validate that a plugin instance has the required methods for its declared category.
|
||||||
const instance = factory()
|
* Called AFTER real instantiation — never used to probe/discover category.
|
||||||
if (!instance || typeof instance !== "object") return false
|
*/
|
||||||
switch (expected) {
|
function validateCategory(
|
||||||
case "pageType":
|
instance: Record<string, unknown>,
|
||||||
return "match" in instance && "body" in instance && "layout" in instance
|
expected: ProcessingCategory,
|
||||||
case "emitter":
|
): boolean {
|
||||||
return "emit" in instance
|
switch (expected) {
|
||||||
case "filter":
|
case "pageType":
|
||||||
return "shouldPublish" in instance
|
return "match" in instance && "body" in instance && "layout" in instance
|
||||||
case "transformer":
|
case "emitter":
|
||||||
return (
|
return "emit" in instance
|
||||||
"textTransform" in instance || "markdownPlugins" in instance || "htmlPlugins" in instance
|
case "filter":
|
||||||
)
|
return "shouldPublish" in instance
|
||||||
}
|
case "transformer":
|
||||||
} catch {
|
return (
|
||||||
return false
|
"textTransform" in instance || "markdownPlugins" in instance || "htmlPlugins" in instance
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the factory function from a plugin module by export convention.
|
||||||
|
* Prefers `default` export, then `plugin` named export, then the sole exported function.
|
||||||
|
* For multi-export modules with an expectedCategory, probes candidate functions to find
|
||||||
|
* the one matching the category shape.
|
||||||
|
*/
|
||||||
function findFactory(
|
function findFactory(
|
||||||
module: Record<string, unknown>,
|
module: Record<string, unknown>,
|
||||||
expectedCategory: ProcessingCategory,
|
expectedCategory?: ProcessingCategory,
|
||||||
): Function | null {
|
): Function | null {
|
||||||
if (
|
if (typeof module.default === "function") {
|
||||||
typeof module.default === "function" &&
|
|
||||||
matchesCategory(module.default as Function, expectedCategory)
|
|
||||||
) {
|
|
||||||
return module.default as Function
|
return module.default as Function
|
||||||
}
|
}
|
||||||
if (
|
if (typeof module.plugin === "function") {
|
||||||
typeof module.plugin === "function" &&
|
|
||||||
matchesCategory(module.plugin as Function, expectedCategory)
|
|
||||||
) {
|
|
||||||
return module.plugin as Function
|
return module.plugin as Function
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [, value] of Object.entries(module)) {
|
const exportedFunctions = Object.entries(module).filter(
|
||||||
if (typeof value === "function" && matchesCategory(value as Function, expectedCategory)) {
|
([key, value]) => typeof value === "function" && !key.startsWith("__"),
|
||||||
return value as Function
|
)
|
||||||
|
|
||||||
|
if (exportedFunctions.length === 1) {
|
||||||
|
return exportedFunctions[0][1] as Function
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiple exports: probe candidates to find the one matching the expected category.
|
||||||
|
// This is the only code path that calls factory() for discovery, and only when
|
||||||
|
// there is no default/plugin export and multiple functions are exported.
|
||||||
|
if (exportedFunctions.length > 1 && expectedCategory) {
|
||||||
|
for (const [, fn] of exportedFunctions) {
|
||||||
|
try {
|
||||||
|
const instance = (fn as Function)()
|
||||||
|
if (
|
||||||
|
instance &&
|
||||||
|
typeof instance === "object" &&
|
||||||
|
validateCategory(instance, expectedCategory)
|
||||||
|
) {
|
||||||
|
return fn as Function
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// This export doesn't work without args — skip it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -539,10 +579,20 @@ 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>
|
||||||
|
|
||||||
if (typeof mod.default === "function") {
|
// Prefer static category marker on the factory if available
|
||||||
// Try to instantiate and inspect
|
const factory = findFactory(mod as Record<string, unknown>)
|
||||||
|
if (factory && "quartzCategory" in factory) {
|
||||||
|
const cat = (factory as Record<string, unknown>).quartzCategory
|
||||||
|
if (cat === "transformer" || cat === "filter" || cat === "emitter" || cat === "pageType") {
|
||||||
|
return cat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try instantiating with no args and inspect the result.
|
||||||
|
// This may fail for plugins that do I/O or require options during construction.
|
||||||
|
if (typeof factory === "function") {
|
||||||
try {
|
try {
|
||||||
const instance = (mod.default as Function)()
|
const instance = factory()
|
||||||
if (instance && typeof instance === "object") {
|
if (instance && typeof instance === "object") {
|
||||||
if ("match" in instance && "body" in instance && "layout" in instance) return "pageType"
|
if ("match" in instance && "body" in instance && "layout" in instance) return "pageType"
|
||||||
if ("emit" in instance) return "emitter"
|
if ("emit" in instance) return "emitter"
|
||||||
@@ -555,7 +605,8 @@ function detectCategoryFromModule(module: unknown): ProcessingCategory | null {
|
|||||||
return "transformer"
|
return "transformer"
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Couldn't instantiate, skip detection
|
// Factory requires arguments or does I/O — cannot detect category by probing.
|
||||||
|
// Plugin should declare category in package.json quartz.category field.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user