diff --git a/quartz/components/registry.ts b/quartz/components/registry.ts index 8aae9aa..2b55b9f 100644 --- a/quartz/components/registry.ts +++ b/quartz/components/registry.ts @@ -19,6 +19,7 @@ export interface RegisteredComponent { class ComponentRegistry { private components = new Map() private instanceCache = new Map() + private optionOverrides = new Map>() register( name: string, @@ -41,6 +42,17 @@ class ComponentRegistry { return new Map(this.components) } + /** Store option overrides for a plugin, keyed by plugin directory name. */ + setOptionOverrides(pluginName: string, opts?: Record): void { + if (!opts || Object.keys(opts).length === 0) return + this.optionOverrides.set(pluginName, { ...this.optionOverrides.get(pluginName), ...opts }) + this.instanceCache.clear() + } + + getOptionOverrides(pluginName: string): Record | undefined { + return this.optionOverrides.get(pluginName) + } + /** * Instantiate a component constructor with options, returning a cached instance * if the same constructor was already called with equivalent options. diff --git a/quartz/plugins/loader/config-loader.ts b/quartz/plugins/loader/config-loader.ts index ad250bb..22e44ef 100644 --- a/quartz/plugins/loader/config-loader.ts +++ b/quartz/plugins/loader/config-loader.ts @@ -349,7 +349,8 @@ export async function loadQuartzConfig( // If the module exports an init() function, call it with merged options // so component-only plugins can receive user configuration from YAML. if (typeof module.init === "function") { - const options = { ...manifest?.defaultOptions, ...entry.options } + const initOverrides = componentRegistry.getOptionOverrides(gitSpec.name) + const options = { ...manifest?.defaultOptions, ...entry.options, ...initOverrides } await module.init(Object.keys(options).length > 0 ? options : undefined) } } catch (e) { @@ -440,7 +441,8 @@ export async function loadQuartzConfig( ) continue } - const options = { ...manifest?.defaultOptions, ...entry.options } + const pluginOverrides = componentRegistry.getOptionOverrides(gitSpec.name) + const options = { ...manifest?.defaultOptions, ...entry.options, ...pluginOverrides } instances.push(factory(Object.keys(options).length > 0 ? options : undefined)) } catch (err) { console.error( @@ -636,7 +638,8 @@ export async function loadQuartzLayout(layoutOverrides?: { if (footerReg) { if (typeof footerReg.component === "function" && !("displayName" in footerReg.component)) { // It's a constructor — use registry cache for consistent instances - const opts = { ...footerEntry.options } + const footerOverrides = componentRegistry.getOptionOverrides("footer") + const opts = { ...footerEntry.options, ...footerOverrides } footer = componentRegistry.instantiate( footerReg.component as QuartzComponentConstructor, Object.keys(opts).length > 0 ? opts : undefined, @@ -726,7 +729,8 @@ function buildLayoutForEntries( if (typeof reg.component === "function" && !("displayName" in reg.component)) { // It's a constructor — use registry cache to avoid duplicate instances // (and duplicate afterDOMLoaded scripts) across page-type layouts - const opts = { ...entry.options } + const tsOverrides = componentRegistry.getOptionOverrides(name) + const opts = { ...entry.options, ...tsOverrides } const optsArg = Object.keys(opts).length > 0 ? opts : undefined component = componentRegistry.instantiate( reg.component as QuartzComponentConstructor, diff --git a/quartz/plugins/loader/gitLoader.ts b/quartz/plugins/loader/gitLoader.ts index 15351ba..f6ce725 100644 --- a/quartz/plugins/loader/gitLoader.ts +++ b/quartz/plugins/loader/gitLoader.ts @@ -829,14 +829,16 @@ export async function regeneratePluginIndex(options: { verbose?: boolean } = {}) return } - const plugins = fs.readdirSync(PLUGINS_CACHE_DIR).filter((name) => { + const pluginDirs = fs.readdirSync(PLUGINS_CACHE_DIR).filter((name) => { const pluginPath = path.join(PLUGINS_CACHE_DIR, name) return fs.statSync(pluginPath).isDirectory() }) - const exports: string[] = [] + // Phase 1: Collect all exports per plugin, detect conflicts + const pluginExports = new Map() + const nameCount = new Map() - for (const pluginName of plugins) { + for (const pluginName of pluginDirs) { const pluginDir = path.join(PLUGINS_CACHE_DIR, pluginName) const distIndex = path.join(pluginDir, "dist", "index.d.ts") @@ -849,27 +851,85 @@ export async function regeneratePluginIndex(options: { verbose?: boolean } = {}) const dtsContent = fs.readFileSync(distIndex, "utf-8") const exportedNames = parseExportsFromDts(dtsContent) + const named = exportedNames.filter((e) => !e.startsWith("type ")) + const types = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5)) - if (exportedNames.length > 0) { - const namedExports = exportedNames.filter((e) => !e.startsWith("type ")) - const typeExports = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5)) - - if (namedExports.length > 0) { - exports.push(`export { ${namedExports.join(", ")} } from "./${pluginName}"`) - } - if (typeExports.length > 0) { - exports.push(`export type { ${typeExports.join(", ")} } from "./${pluginName}"`) + if (named.length > 0 || types.length > 0) { + pluginExports.set(pluginName, { named, types }) + for (const n of named) { + nameCount.set(n, (nameCount.get(n) ?? 0) + 1) } } } - const indexContent = exports.join("\n") + "\n" + // Phase 2: Generate index with registry import, plugin map, and conditional top-level exports + const lines: string[] = [] + + lines.push(`import { componentRegistry } from "../../quartz/components/registry"`) + lines.push("") + + // Type re-exports + for (const [pluginName, { types }] of pluginExports) { + if (types.length > 0) { + lines.push(`export type { ${types.join(", ")} } from "./${pluginName}"`) + } + } + lines.push("") + + // Generate the plugins map with override wrappers + lines.push( + `export const plugins: Record void>> = {`, + ) + for (const [pluginName, { named }] of pluginExports) { + if (named.length === 0) continue + const escapedName = pluginName.replace(/"/g, '\\"') + lines.push(` "${escapedName}": {`) + for (const n of named) { + lines.push( + ` ${n}: (...args: unknown[]) => { componentRegistry.setOptionOverrides("${escapedName}", args[0] as Record); },`, + ) + } + lines.push(` },`) + } + lines.push(`}`) + lines.push("") + + // Top-level exports: only for non-conflicting names + for (const [pluginName, { named }] of pluginExports) { + if (named.length === 0) continue + + const unique = named.filter((n) => (nameCount.get(n) ?? 0) === 1) + const conflicting = named.filter((n) => (nameCount.get(n) ?? 0) > 1) + + if (unique.length > 0) { + const escapedName = pluginName.replace(/"/g, '\\"') + for (const n of unique) { + lines.push(`export const ${n} = plugins["${escapedName}"].${n}`) + } + } + + if (conflicting.length > 0 && options.verbose) { + for (const n of conflicting) { + console.warn( + styleText("yellow", `⚠`), + `Export "${n}" conflicts across plugins — use plugins["${pluginName}"].${n} in quartz.ts`, + ) + } + } + } + + lines.push("") + + const indexContent = lines.join("\n") const indexPath = path.join(PLUGINS_CACHE_DIR, "index.ts") fs.writeFileSync(indexPath, indexContent) if (options.verbose) { - console.log(styleText("green", `✓`), `Regenerated plugin index with ${plugins.length} plugins`) + console.log( + styleText("green", `✓`), + `Regenerated plugin index with ${pluginDirs.length} plugins`, + ) } }