mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat(plugins): v5 plugin system
This commit is contained in:
parent
ec00a40aef
commit
4066ffdace
@ -8,7 +8,21 @@ import {
|
|||||||
handleRestore,
|
handleRestore,
|
||||||
handleSync,
|
handleSync,
|
||||||
} from "./cli/handlers.js"
|
} from "./cli/handlers.js"
|
||||||
import { CommonArgv, BuildArgv, CreateArgv, SyncArgv } from "./cli/args.js"
|
import {
|
||||||
|
handlePluginInstall,
|
||||||
|
handlePluginList,
|
||||||
|
handlePluginSearch,
|
||||||
|
handlePluginUninstall,
|
||||||
|
} from "./cli/plugin-handlers.js"
|
||||||
|
import {
|
||||||
|
CommonArgv,
|
||||||
|
BuildArgv,
|
||||||
|
CreateArgv,
|
||||||
|
SyncArgv,
|
||||||
|
PluginInstallArgv,
|
||||||
|
PluginUninstallArgv,
|
||||||
|
PluginSearchArgv,
|
||||||
|
} from "./cli/args.js"
|
||||||
import { version } from "./cli/constants.js"
|
import { version } from "./cli/constants.js"
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
@ -35,6 +49,44 @@ yargs(hideBin(process.argv))
|
|||||||
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
.command("build", "Build Quartz into a bundle of static HTML files", BuildArgv, async (argv) => {
|
||||||
await handleBuild(argv)
|
await handleBuild(argv)
|
||||||
})
|
})
|
||||||
|
.command(
|
||||||
|
"plugin <subcommand>",
|
||||||
|
"Manage Quartz plugins",
|
||||||
|
(yargs) => {
|
||||||
|
return yargs
|
||||||
|
.command(
|
||||||
|
"install <packages..>",
|
||||||
|
"Install Quartz plugins from npm",
|
||||||
|
PluginInstallArgv,
|
||||||
|
async (argv) => {
|
||||||
|
await handlePluginInstall(argv.packages)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.command(
|
||||||
|
"uninstall <packages..>",
|
||||||
|
"Uninstall Quartz plugins",
|
||||||
|
PluginUninstallArgv,
|
||||||
|
async (argv) => {
|
||||||
|
await handlePluginUninstall(argv.packages)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.command("list", "List installed Quartz plugins", CommonArgv, async () => {
|
||||||
|
await handlePluginList()
|
||||||
|
})
|
||||||
|
.command(
|
||||||
|
"search [query]",
|
||||||
|
"Search for Quartz plugins on npm",
|
||||||
|
PluginSearchArgv,
|
||||||
|
async (argv) => {
|
||||||
|
await handlePluginSearch(argv.query)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.demandCommand(1, "Please specify a plugin subcommand")
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// This handler is called when no subcommand is provided
|
||||||
|
},
|
||||||
|
)
|
||||||
.showHelpOnFail(false)
|
.showHelpOnFail(false)
|
||||||
.help()
|
.help()
|
||||||
.strict()
|
.strict()
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ValidDateType } from "./components/Date"
|
import { ValidDateType } from "./components/Date"
|
||||||
import { QuartzComponent } from "./components/types"
|
import { QuartzComponent } from "./components/types"
|
||||||
import { ValidLocale } from "./i18n"
|
import { ValidLocale } from "./i18n"
|
||||||
|
import { PluginSpecifier } from "./plugins/loader/types"
|
||||||
import { PluginTypes } from "./plugins/types"
|
import { PluginTypes } from "./plugins/types"
|
||||||
import { Theme } from "./util/theme"
|
import { Theme } from "./util/theme"
|
||||||
|
|
||||||
@ -88,6 +89,7 @@ export interface GlobalConfiguration {
|
|||||||
export interface QuartzConfig {
|
export interface QuartzConfig {
|
||||||
configuration: GlobalConfiguration
|
configuration: GlobalConfiguration
|
||||||
plugins: PluginTypes
|
plugins: PluginTypes
|
||||||
|
externalPlugins?: PluginSpecifier[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FullPageLayout {
|
export interface FullPageLayout {
|
||||||
|
|||||||
@ -106,3 +106,30 @@ export const BuildArgv = {
|
|||||||
describe: "how many threads to use to parse notes",
|
describe: "how many threads to use to parse notes",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PluginInstallArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
_: {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "package names to install",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PluginUninstallArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
_: {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "package names to uninstall",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PluginSearchArgv = {
|
||||||
|
...CommonArgv,
|
||||||
|
query: {
|
||||||
|
string: true,
|
||||||
|
alias: ["q"],
|
||||||
|
describe: "search query for plugins",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|||||||
130
quartz/cli/plugin-handlers.js
Normal file
130
quartz/cli/plugin-handlers.js
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { styleText } from "util"
|
||||||
|
import { execSync, spawnSync } from "child_process"
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
|
||||||
|
export async function handlePluginInstall(packageNames) {
|
||||||
|
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
|
||||||
|
|
||||||
|
if (packageNames.length === 0) {
|
||||||
|
console.log(styleText("red", "Error: No package names provided"))
|
||||||
|
console.log("Usage: npx quartz plugin install <package-name> [package-name...]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installing ${packageNames.length} plugin(s)...`)
|
||||||
|
|
||||||
|
const npmArgs = ["install", ...packageNames]
|
||||||
|
const result = spawnSync("npm", npmArgs, { stdio: "inherit" })
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.log(styleText("red", "Failed to install plugins"))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("green", "✓ Plugins installed successfully"))
|
||||||
|
console.log("\nAdd them to your quartz.config.ts:")
|
||||||
|
|
||||||
|
for (const pkg of packageNames) {
|
||||||
|
console.log(` import { Plugin } from "${pkg}"`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginList() {
|
||||||
|
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
|
||||||
|
|
||||||
|
const packageJsonPath = path.join(process.cwd(), "package.json")
|
||||||
|
|
||||||
|
if (!fs.existsSync(packageJsonPath)) {
|
||||||
|
console.log(styleText("red", "No package.json found"))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
|
||||||
|
const allDeps = {
|
||||||
|
...packageJson.dependencies,
|
||||||
|
...packageJson.devDependencies,
|
||||||
|
}
|
||||||
|
|
||||||
|
const quartzPlugins = Object.entries(allDeps).filter(([name]) => {
|
||||||
|
return (
|
||||||
|
name.startsWith("@quartz/") ||
|
||||||
|
name.startsWith("quartz-") ||
|
||||||
|
name.startsWith("@quartz-community/")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (quartzPlugins.length === 0) {
|
||||||
|
console.log("No Quartz plugins found in this project.")
|
||||||
|
console.log("Install plugins with: npx quartz plugin install <package-name>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${quartzPlugins.length} Quartz plugin(s):\n`)
|
||||||
|
|
||||||
|
for (const [name, version] of quartzPlugins) {
|
||||||
|
console.log(` ${styleText("cyan", name)}@${version}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginSearch(query) {
|
||||||
|
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
|
||||||
|
|
||||||
|
const searchQuery = query || "quartz-plugin"
|
||||||
|
|
||||||
|
console.log(`Searching npm for packages matching "${searchQuery}"...`)
|
||||||
|
console.log(styleText("grey", "(This may take a moment)\n"))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = execSync(`npm search ${searchQuery} --json`, { encoding: "utf-8" })
|
||||||
|
const packages = JSON.parse(result)
|
||||||
|
|
||||||
|
const quartzPlugins = packages.filter(
|
||||||
|
(pkg) =>
|
||||||
|
pkg.name.startsWith("@quartz/") ||
|
||||||
|
pkg.name.startsWith("quartz-") ||
|
||||||
|
pkg.name.startsWith("@quartz-community/"),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (quartzPlugins.length === 0) {
|
||||||
|
console.log("No Quartz plugins found matching your query.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Found ${quartzPlugins.length} Quartz plugin(s):\n`)
|
||||||
|
|
||||||
|
for (const pkg of quartzPlugins.slice(0, 20)) {
|
||||||
|
console.log(` ${styleText("cyan", pkg.name)}@${pkg.version}`)
|
||||||
|
if (pkg.description) {
|
||||||
|
console.log(` ${styleText("grey", pkg.description)}`)
|
||||||
|
}
|
||||||
|
console.log()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.log(styleText("yellow", "Could not search npm. Try visiting:"))
|
||||||
|
console.log(" https://www.npmjs.com/search?q=quartz-plugin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handlePluginUninstall(packageNames) {
|
||||||
|
console.log(`\n${styleText(["bgGreen", "black"], " Quartz Plugin Manager ")}\n`)
|
||||||
|
|
||||||
|
if (packageNames.length === 0) {
|
||||||
|
console.log(styleText("red", "Error: No package names provided"))
|
||||||
|
console.log("Usage: npx quartz plugin uninstall <package-name> [package-name...]")
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Uninstalling ${packageNames.length} plugin(s)...`)
|
||||||
|
|
||||||
|
const npmArgs = ["uninstall", ...packageNames]
|
||||||
|
const result = spawnSync("npm", npmArgs, { stdio: "inherit" })
|
||||||
|
|
||||||
|
if (result.status !== 0) {
|
||||||
|
console.log(styleText("red", "Failed to uninstall plugins"))
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(styleText("green", "✓ Plugins uninstalled successfully"))
|
||||||
|
console.log(styleText("yellow", "Don't forget to remove them from your quartz.config.ts!"))
|
||||||
|
}
|
||||||
23
quartz/components/external.ts
Normal file
23
quartz/components/external.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { componentRegistry } from "./registry"
|
||||||
|
import { QuartzComponent, QuartzComponentConstructor } from "./types"
|
||||||
|
|
||||||
|
export function External<Options extends object | undefined>(
|
||||||
|
name: string,
|
||||||
|
options?: Options,
|
||||||
|
): QuartzComponent {
|
||||||
|
const registered = componentRegistry.get(name)
|
||||||
|
if (!registered) {
|
||||||
|
throw new Error(
|
||||||
|
`External component "${name}" not found. ` +
|
||||||
|
`Make sure the plugin is installed and components are loaded before layouts are evaluated.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { component } = registered
|
||||||
|
|
||||||
|
if (typeof component === "function") {
|
||||||
|
return (component as QuartzComponentConstructor<Options>)(options as Options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return component as QuartzComponent
|
||||||
|
}
|
||||||
@ -24,6 +24,11 @@ import Comments from "./Comments"
|
|||||||
import Flex from "./Flex"
|
import Flex from "./Flex"
|
||||||
import ConditionalRender from "./ConditionalRender"
|
import ConditionalRender from "./ConditionalRender"
|
||||||
|
|
||||||
|
export { componentRegistry, defineComponent } from "./registry"
|
||||||
|
export { External } from "./external"
|
||||||
|
export type { ComponentManifest, RegisteredComponent } from "./registry"
|
||||||
|
export type { QuartzComponent, QuartzComponentConstructor, QuartzComponentProps } from "./types"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
ArticleTitle,
|
ArticleTitle,
|
||||||
Content,
|
Content,
|
||||||
|
|||||||
60
quartz/components/registry.ts
Normal file
60
quartz/components/registry.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { QuartzComponent, QuartzComponentConstructor } from "./types"
|
||||||
|
|
||||||
|
export interface ComponentManifest {
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
quartzVersion?: string
|
||||||
|
author?: string
|
||||||
|
homepage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RegisteredComponent {
|
||||||
|
component: QuartzComponent | QuartzComponentConstructor
|
||||||
|
source: string
|
||||||
|
manifest?: ComponentManifest
|
||||||
|
}
|
||||||
|
|
||||||
|
class ComponentRegistry {
|
||||||
|
private components = new Map<string, RegisteredComponent>()
|
||||||
|
|
||||||
|
register(
|
||||||
|
name: string,
|
||||||
|
component: QuartzComponent | QuartzComponentConstructor,
|
||||||
|
source: string,
|
||||||
|
manifest?: ComponentManifest,
|
||||||
|
): void {
|
||||||
|
if (this.components.has(name)) {
|
||||||
|
console.warn(`Component "${name}" is being overwritten by ${source}`)
|
||||||
|
}
|
||||||
|
this.components.set(name, { component, source, manifest })
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): RegisteredComponent | undefined {
|
||||||
|
return this.components.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(): Map<string, RegisteredComponent> {
|
||||||
|
return new Map(this.components)
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllComponents(): QuartzComponent[] {
|
||||||
|
return Array.from(this.components.values()).map((r) => {
|
||||||
|
if (typeof r.component === "function") {
|
||||||
|
return (r.component as QuartzComponentConstructor)(undefined)
|
||||||
|
}
|
||||||
|
return r.component as QuartzComponent
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const componentRegistry = new ComponentRegistry()
|
||||||
|
|
||||||
|
export function defineComponent<Options extends object | undefined = undefined>(
|
||||||
|
factory: QuartzComponentConstructor<Options>,
|
||||||
|
manifest: ComponentManifest,
|
||||||
|
): QuartzComponentConstructor<Options> {
|
||||||
|
;(factory as any).__quartzComponent = { manifest }
|
||||||
|
return factory
|
||||||
|
}
|
||||||
40
quartz/plugins/config.ts
Normal file
40
quartz/plugins/config.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
QuartzTransformerPluginInstance,
|
||||||
|
QuartzFilterPluginInstance,
|
||||||
|
QuartzEmitterPluginInstance,
|
||||||
|
} from "./types"
|
||||||
|
import { LoadedPlugin } from "./loader/types"
|
||||||
|
|
||||||
|
export interface PluginConfiguration {
|
||||||
|
transformers: (QuartzTransformerPluginInstance | LoadedPlugin)[]
|
||||||
|
filters: (QuartzFilterPluginInstance | LoadedPlugin)[]
|
||||||
|
emitters: (QuartzEmitterPluginInstance | LoadedPlugin)[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isLoadedPlugin(plugin: unknown): plugin is LoadedPlugin {
|
||||||
|
return (
|
||||||
|
typeof plugin === "object" &&
|
||||||
|
plugin !== null &&
|
||||||
|
"plugin" in plugin &&
|
||||||
|
"manifest" in plugin &&
|
||||||
|
"type" in plugin &&
|
||||||
|
typeof (plugin as LoadedPlugin).plugin === "function"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPluginInstance<T extends object | undefined>(
|
||||||
|
plugin:
|
||||||
|
| QuartzTransformerPluginInstance
|
||||||
|
| QuartzFilterPluginInstance
|
||||||
|
| QuartzEmitterPluginInstance
|
||||||
|
| LoadedPlugin,
|
||||||
|
options?: T,
|
||||||
|
): QuartzTransformerPluginInstance | QuartzFilterPluginInstance | QuartzEmitterPluginInstance {
|
||||||
|
if (isLoadedPlugin(plugin)) {
|
||||||
|
const factory = plugin.plugin as (
|
||||||
|
opts?: T,
|
||||||
|
) => QuartzTransformerPluginInstance | QuartzFilterPluginInstance | QuartzEmitterPluginInstance
|
||||||
|
return factory(options)
|
||||||
|
}
|
||||||
|
return plugin
|
||||||
|
}
|
||||||
@ -9,6 +9,7 @@ import styles from "../../styles/custom.scss"
|
|||||||
import popoverStyle from "../../components/styles/popover.scss"
|
import popoverStyle from "../../components/styles/popover.scss"
|
||||||
import { BuildCtx } from "../../util/ctx"
|
import { BuildCtx } from "../../util/ctx"
|
||||||
import { QuartzComponent } from "../../components/types"
|
import { QuartzComponent } from "../../components/types"
|
||||||
|
import { componentRegistry } from "../../components/registry"
|
||||||
import {
|
import {
|
||||||
googleFontHref,
|
googleFontHref,
|
||||||
googleFontSubsetHref,
|
googleFontSubsetHref,
|
||||||
@ -27,6 +28,7 @@ type ComponentResources = {
|
|||||||
|
|
||||||
function getComponentResources(ctx: BuildCtx): ComponentResources {
|
function getComponentResources(ctx: BuildCtx): ComponentResources {
|
||||||
const allComponents: Set<QuartzComponent> = new Set()
|
const allComponents: Set<QuartzComponent> = new Set()
|
||||||
|
|
||||||
for (const emitter of ctx.cfg.plugins.emitters) {
|
for (const emitter of ctx.cfg.plugins.emitters) {
|
||||||
const components = emitter.getQuartzComponents?.(ctx) ?? []
|
const components = emitter.getQuartzComponents?.(ctx) ?? []
|
||||||
for (const component of components) {
|
for (const component of components) {
|
||||||
@ -34,6 +36,10 @@ function getComponentResources(ctx: BuildCtx): ComponentResources {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const component of componentRegistry.getAllComponents()) {
|
||||||
|
allComponents.add(component)
|
||||||
|
}
|
||||||
|
|
||||||
const componentResources = {
|
const componentResources = {
|
||||||
css: new Set<string>(),
|
css: new Set<string>(),
|
||||||
beforeDOMLoaded: new Set<string>(),
|
beforeDOMLoaded: new Set<string>(),
|
||||||
|
|||||||
@ -45,6 +45,9 @@ export function getStaticResourcesFromPlugins(ctx: BuildCtx) {
|
|||||||
export * from "./transformers"
|
export * from "./transformers"
|
||||||
export * from "./filters"
|
export * from "./filters"
|
||||||
export * from "./emitters"
|
export * from "./emitters"
|
||||||
|
export * from "./types"
|
||||||
|
export * from "./config"
|
||||||
|
export * as PluginLoader from "./loader"
|
||||||
|
|
||||||
declare module "vfile" {
|
declare module "vfile" {
|
||||||
// inserted in processors.ts
|
// inserted in processors.ts
|
||||||
|
|||||||
39
quartz/plugins/loader/componentLoader.ts
Normal file
39
quartz/plugins/loader/componentLoader.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { componentRegistry } from "../../components/registry"
|
||||||
|
import { ComponentManifest, PluginManifest } from "./types"
|
||||||
|
import { QuartzComponentConstructor } from "../../components/types"
|
||||||
|
|
||||||
|
export async function loadComponentsFromPackage(
|
||||||
|
packageName: string,
|
||||||
|
manifest: PluginManifest | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (!manifest?.components) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const componentsModule = await import(`${packageName}/components`)
|
||||||
|
|
||||||
|
for (const [exportName, componentManifest] of Object.entries(manifest.components)) {
|
||||||
|
const component = componentsModule[exportName]
|
||||||
|
if (!component) {
|
||||||
|
console.warn(
|
||||||
|
`Component "${exportName}" declared in manifest but not found in ${packageName}/components`,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullName = `${packageName}/${exportName}`
|
||||||
|
componentRegistry.register(
|
||||||
|
fullName,
|
||||||
|
component as QuartzComponentConstructor,
|
||||||
|
packageName,
|
||||||
|
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) {
|
||||||
|
console.warn(
|
||||||
|
`Plugin ${packageName} declares components but failed to load them from ${packageName}/components`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
293
quartz/plugins/loader/index.ts
Normal file
293
quartz/plugins/loader/index.ts
Normal file
@ -0,0 +1,293 @@
|
|||||||
|
import { styleText } from "util"
|
||||||
|
import {
|
||||||
|
PluginManifest,
|
||||||
|
LoadedPlugin,
|
||||||
|
PluginResolution,
|
||||||
|
PluginResolutionError,
|
||||||
|
PluginResolutionOptions,
|
||||||
|
PluginSpecifier,
|
||||||
|
} from "./types"
|
||||||
|
import { QuartzTransformerPlugin, QuartzFilterPlugin, QuartzEmitterPlugin } from "../types"
|
||||||
|
|
||||||
|
const MINIMUM_QUARTZ_VERSION = "4.5.0"
|
||||||
|
|
||||||
|
function satisfiesVersion(required: string | undefined, current: string): boolean {
|
||||||
|
if (!required) return true
|
||||||
|
|
||||||
|
const parseVersion = (v: string) => {
|
||||||
|
const parts = v.replace(/^v/, "").split(".")
|
||||||
|
return {
|
||||||
|
major: parseInt(parts[0]) || 0,
|
||||||
|
minor: parseInt(parts[1]) || 0,
|
||||||
|
patch: parseInt(parts[2]) || 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = parseVersion(required)
|
||||||
|
const cur = parseVersion(current)
|
||||||
|
|
||||||
|
if (cur.major > req.major) return true
|
||||||
|
if (cur.major < req.major) return false
|
||||||
|
if (cur.minor > req.minor) return true
|
||||||
|
if (cur.minor < req.minor) return false
|
||||||
|
return cur.patch >= req.patch
|
||||||
|
}
|
||||||
|
|
||||||
|
async function tryImportPlugin(packageName: string): Promise<{
|
||||||
|
module: unknown
|
||||||
|
manifest: PluginManifest | null
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
const module = await import(packageName)
|
||||||
|
|
||||||
|
const manifest: PluginManifest | null = module.manifest ?? null
|
||||||
|
|
||||||
|
return { module, manifest }
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to import package: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectPluginType(module: unknown): "transformer" | "filter" | "emitter" | null {
|
||||||
|
if (!module || typeof module !== "object") return null
|
||||||
|
|
||||||
|
const mod = module as Record<string, unknown>
|
||||||
|
|
||||||
|
if (typeof mod.default === "function") {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasTransformerProps = ["textTransform", "markdownPlugins", "htmlPlugins"].some(
|
||||||
|
(key) => key in mod && (typeof mod[key] === "function" || mod[key] === undefined),
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasFilterProps = ["shouldPublish"].some(
|
||||||
|
(key) => key in mod && typeof mod[key] === "function",
|
||||||
|
)
|
||||||
|
|
||||||
|
const hasEmitterProps = ["emit"].some((key) => key in mod && typeof mod[key] === "function")
|
||||||
|
|
||||||
|
if (hasEmitterProps) return "emitter"
|
||||||
|
if (hasFilterProps) return "filter"
|
||||||
|
if (hasTransformerProps) return "transformer"
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractPluginFactory(
|
||||||
|
module: unknown,
|
||||||
|
type: "transformer" | "filter" | "emitter",
|
||||||
|
): QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin | null {
|
||||||
|
if (!module || typeof module !== "object") return null
|
||||||
|
|
||||||
|
const mod = module as Record<string, unknown>
|
||||||
|
|
||||||
|
const factory = mod.default ?? mod[type] ?? mod.plugin ?? null
|
||||||
|
|
||||||
|
if (typeof factory === "function") {
|
||||||
|
return factory as QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveSinglePlugin(
|
||||||
|
specifier: PluginSpecifier,
|
||||||
|
options: PluginResolutionOptions,
|
||||||
|
): Promise<{ plugin: LoadedPlugin | null; error: PluginResolutionError | null }> {
|
||||||
|
let packageName: string
|
||||||
|
let manifest: Partial<PluginManifest> = {}
|
||||||
|
|
||||||
|
if (typeof specifier === "string") {
|
||||||
|
packageName = specifier
|
||||||
|
} else if ("name" in specifier) {
|
||||||
|
packageName = specifier.name
|
||||||
|
} else if ("plugin" in specifier) {
|
||||||
|
const type = specifier.manifest?.category ?? "transformer"
|
||||||
|
return {
|
||||||
|
plugin: {
|
||||||
|
plugin: specifier.plugin as QuartzTransformerPlugin,
|
||||||
|
manifest: {
|
||||||
|
name: specifier.manifest?.name ?? "inline-plugin",
|
||||||
|
displayName: specifier.manifest?.displayName ?? "Inline Plugin",
|
||||||
|
description: specifier.manifest?.description ?? "Inline plugin instance",
|
||||||
|
version: specifier.manifest?.version ?? "1.0.0",
|
||||||
|
category: type as "transformer" | "filter" | "emitter",
|
||||||
|
...specifier.manifest,
|
||||||
|
} as PluginManifest,
|
||||||
|
type: type as "transformer" | "filter" | "emitter",
|
||||||
|
source: "inline",
|
||||||
|
},
|
||||||
|
error: null,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: "unknown",
|
||||||
|
message: "Invalid plugin specifier format",
|
||||||
|
type: "invalid-manifest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { module: importedModule, manifest: importedManifest } =
|
||||||
|
await tryImportPlugin(packageName)
|
||||||
|
|
||||||
|
manifest = importedManifest ?? {}
|
||||||
|
|
||||||
|
// Load components if the plugin declares any
|
||||||
|
if (manifest.components && Object.keys(manifest.components).length > 0) {
|
||||||
|
const { loadComponentsFromPackage } = await import("./componentLoader")
|
||||||
|
await loadComponentsFromPackage(packageName, manifest as PluginManifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
const detectedType = manifest.category ?? detectPluginType(importedModule)
|
||||||
|
|
||||||
|
if (!detectedType) {
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: packageName,
|
||||||
|
message: `Could not detect plugin type. Ensure the plugin exports a valid factory function or has a 'category' field in its manifest.`,
|
||||||
|
type: "invalid-manifest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
manifest.quartzVersion &&
|
||||||
|
!satisfiesVersion(manifest.quartzVersion, options.quartzVersion)
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: packageName,
|
||||||
|
message: `Plugin requires Quartz ${manifest.quartzVersion} but current version is ${options.quartzVersion}`,
|
||||||
|
type: "version-mismatch",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const factory = extractPluginFactory(importedModule, detectedType)
|
||||||
|
|
||||||
|
if (!factory) {
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: packageName,
|
||||||
|
message: `Could not find plugin factory in module. Expected 'export default' or '${detectedType}' export.`,
|
||||||
|
type: "invalid-manifest",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullManifest: PluginManifest = {
|
||||||
|
name: manifest.name ?? packageName,
|
||||||
|
displayName: manifest.displayName ?? packageName,
|
||||||
|
description: manifest.description ?? "No description provided",
|
||||||
|
version: manifest.version ?? "1.0.0",
|
||||||
|
author: manifest.author,
|
||||||
|
homepage: manifest.homepage,
|
||||||
|
keywords: manifest.keywords,
|
||||||
|
category: manifest.category ?? detectedType,
|
||||||
|
quartzVersion: manifest.quartzVersion,
|
||||||
|
configSchema: manifest.configSchema,
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedPlugin: LoadedPlugin = {
|
||||||
|
plugin: factory,
|
||||||
|
manifest: fullManifest,
|
||||||
|
type: detectedType,
|
||||||
|
source: packageName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(
|
||||||
|
styleText("green", `✓`) +
|
||||||
|
` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { plugin: loadedPlugin, error: null }
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
|
||||||
|
if (errorMessage.includes("Cannot find module") || errorMessage.includes("MODULE_NOT_FOUND")) {
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: packageName,
|
||||||
|
message: `Plugin package not found. Run 'npm install ${packageName}' to install it.`,
|
||||||
|
type: "not-found",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
plugin: null,
|
||||||
|
error: {
|
||||||
|
plugin: packageName,
|
||||||
|
message: errorMessage,
|
||||||
|
type: "import-error",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolvePlugins(
|
||||||
|
specifiers: PluginSpecifier[],
|
||||||
|
options: PluginResolutionOptions,
|
||||||
|
): Promise<PluginResolution> {
|
||||||
|
const plugins: LoadedPlugin[] = []
|
||||||
|
const errors: PluginResolutionError[] = []
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(styleText("cyan", `Resolving ${specifiers.length} external plugin(s)...`))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const specifier of specifiers) {
|
||||||
|
const { plugin, error } = await resolveSinglePlugin(specifier, options)
|
||||||
|
|
||||||
|
if (plugin) {
|
||||||
|
plugins.push(plugin)
|
||||||
|
} else if (error) {
|
||||||
|
errors.push(error)
|
||||||
|
console.error(
|
||||||
|
styleText("red", `✗`) +
|
||||||
|
` Failed to load plugin: ${styleText("yellow", error.plugin)}\n` +
|
||||||
|
` ${error.message}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.verbose && plugins.length > 0) {
|
||||||
|
const byType = plugins.reduce(
|
||||||
|
(acc, p) => {
|
||||||
|
acc[p.type] = (acc[p.type] || 0) + 1
|
||||||
|
return acc
|
||||||
|
},
|
||||||
|
{} as Record<string, number>,
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
styleText("cyan", `External plugins loaded:`) +
|
||||||
|
` ${byType.transformer ?? 0} transformers, ${byType.filter ?? 0} filters, ${byType.emitter ?? 0} emitters`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { plugins, errors }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function instantiatePlugin<T>(
|
||||||
|
loadedPlugin: LoadedPlugin,
|
||||||
|
options?: T,
|
||||||
|
): ReturnType<typeof loadedPlugin.plugin> {
|
||||||
|
const factory = loadedPlugin.plugin as (opts?: T) => ReturnType<typeof loadedPlugin.plugin>
|
||||||
|
return factory(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { satisfiesVersion, MINIMUM_QUARTZ_VERSION }
|
||||||
2
quartz/plugins/loader/loader.ts
Normal file
2
quartz/plugins/loader/loader.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./types"
|
||||||
|
export * from "./index"
|
||||||
88
quartz/plugins/loader/types.ts
Normal file
88
quartz/plugins/loader/types.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { QuartzTransformerPlugin, QuartzFilterPlugin, QuartzEmitterPlugin } from "../types"
|
||||||
|
import { BuildCtx } from "../../util/ctx"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component manifest metadata
|
||||||
|
*/
|
||||||
|
export interface ComponentManifest {
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
quartzVersion?: string
|
||||||
|
author?: string
|
||||||
|
homepage?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin manifest metadata for discovery and documentation
|
||||||
|
*/
|
||||||
|
export interface PluginManifest {
|
||||||
|
name: string
|
||||||
|
displayName: string
|
||||||
|
description: string
|
||||||
|
version: string
|
||||||
|
author?: string
|
||||||
|
homepage?: string
|
||||||
|
keywords?: string[]
|
||||||
|
category?: "transformer" | "filter" | "emitter"
|
||||||
|
quartzVersion?: string
|
||||||
|
configSchema?: object
|
||||||
|
/** Components provided by this plugin */
|
||||||
|
components?: Record<string, ComponentManifest>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loaded plugin with metadata
|
||||||
|
*/
|
||||||
|
export interface LoadedPlugin {
|
||||||
|
plugin: QuartzTransformerPlugin | QuartzFilterPlugin | QuartzEmitterPlugin
|
||||||
|
manifest: PluginManifest
|
||||||
|
type: "transformer" | "filter" | "emitter"
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin resolution result
|
||||||
|
*/
|
||||||
|
export interface PluginResolution {
|
||||||
|
/** Successfully loaded plugins */
|
||||||
|
plugins: LoadedPlugin[]
|
||||||
|
/** Errors that occurred during resolution */
|
||||||
|
errors: PluginResolutionError[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin resolution error
|
||||||
|
*/
|
||||||
|
export interface PluginResolutionError {
|
||||||
|
/** Plugin name that failed to load */
|
||||||
|
plugin: string
|
||||||
|
/** Error message */
|
||||||
|
message: string
|
||||||
|
/** Error type */
|
||||||
|
type: "not-found" | "invalid-manifest" | "version-mismatch" | "import-error"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options for plugin resolution
|
||||||
|
*/
|
||||||
|
export interface PluginResolutionOptions {
|
||||||
|
/** Current Quartz version for compatibility checking */
|
||||||
|
quartzVersion: string
|
||||||
|
/** Build context for logging */
|
||||||
|
ctx: BuildCtx
|
||||||
|
/** Whether to enable verbose logging */
|
||||||
|
verbose?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plugin specifier - can be:
|
||||||
|
* - String package name (e.g., "@quartz-community/my-plugin")
|
||||||
|
* - Object with name and options (e.g., { name: "@quartz-community/my-plugin", options: {...} })
|
||||||
|
* - Inline plugin object (already loaded plugin instance)
|
||||||
|
*/
|
||||||
|
export type PluginSpecifier =
|
||||||
|
| string
|
||||||
|
| { name: string; options?: unknown }
|
||||||
|
| { plugin: LoadedPlugin["plugin"]; manifest?: Partial<PluginManifest> }
|
||||||
Loading…
Reference in New Issue
Block a user