mirror of
https://github.com/jackyzha0/quartz.git
synced 2025-12-20 03:14:06 -06:00
* Initial plan * docs: format design document for decoupling strategy Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * feat: implement Phase 1-4 of plugin decoupling strategy - Created centralized vfile-schema.ts with all VFile data type definitions - Created plugin-context.ts with PluginUtilities abstraction layer - Created test-helpers.ts for plugin testing utilities - Made BuildCtx readonly to prevent plugin mutations - Added MutableBuildCtx for build orchestration layer - Updated components to not mutate ctx.trie - Updated FrontMatter plugin with temporary cast for backward compatibility - Updated TransformOptions to accept ReadonlyArray Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * feat: add plugin utilities to BuildCtx and document plugin decoupling - Added utils property to BuildCtx with createPluginUtilities() - Documented plugin data dependencies with JSDoc comments on transformers and filters - Created comprehensive plugin migration guide in docs/PLUGIN_MIGRATION.md - Updated plugin types.ts with guidance on using PluginContext - All plugins now have access to ctx.utils for path, resource, and escape utilities Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * docs: add implementation and security summaries - Created IMPLEMENTATION_SUMMARY.md documenting all phases completed - Created SECURITY_SUMMARY.md with CodeQL scan results - All security checks passed with 0 vulnerabilities - Complete documentation of changes, decisions, and impact Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * docs: add guidance for custom plugins extending vfile data - Added section "Adding Custom VFile Fields" to PLUGIN_MIGRATION.md - Demonstrates TypeScript module augmentation for custom fields - Shows best practices for extending vfile data in third-party plugins - Clarifies that centralized schema doesn't prevent custom extensions Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> * refactor: address code review feedback - Remove `any` type annotations from component map callbacks (Breadcrumbs, FolderContent) - Fix test-helpers: change `config` to `cfg` to match BuildCtx interface - Add `trie: undefined` to mock context for completeness - Add missing vfile fields to schema: text, blocks, htmlAst, hasMermaidDiagram - Make Argv properties truly readonly with ReadonlyArgv type - Add documentation explaining module augmentation pattern is intentional - Document FrontMatter mutation as known technical debt with timeline Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>
6.4 KiB
6.4 KiB
Plugin Decoupling Migration Guide
This guide helps plugin authors migrate to the new decoupled plugin architecture introduced by the plugin decoupling strategy.
Overview
The plugin system has been enhanced with:
- Centralized VFile Schema (
quartz/plugins/vfile-schema.ts): Type-safe access to vfile data - Plugin Utilities (
quartz/plugins/plugin-context.ts): Abstracted utility functions viactx.utils - Test Helpers (
quartz/plugins/test-helpers.ts): Mock utilities for testing plugins - Readonly BuildCtx: Prevents accidental mutations from plugins
Using the New Plugin Utilities
Before: Direct Imports
import { QuartzTransformerPlugin } from "../types"
import { simplifySlug, transformLink, pathToRoot } from "../../util/path"
export const MyPlugin: QuartzTransformerPlugin = () => ({
name: "MyPlugin",
htmlPlugins(ctx) {
// Direct utility imports
const slug = simplifySlug(someSlug)
const link = transformLink(from, to, opts)
const root = pathToRoot(slug)
// ...
},
})
After: Using ctx.utils (Optional, Recommended for New Plugins)
import { QuartzTransformerPlugin } from "../types"
export const MyPlugin: QuartzTransformerPlugin = () => ({
name: "MyPlugin",
htmlPlugins(ctx) {
// Use utilities from context (no imports needed)
const slug = ctx.utils!.path.simplify(someSlug)
const link = ctx.utils!.path.transform(from, to, opts)
const root = ctx.utils!.path.toRoot(slug)
// ...
},
})
Available Utilities
Path Utilities (ctx.utils.path)
ctx.utils.path.slugify(path: FilePath) => FullSlug
ctx.utils.path.simplify(slug: FullSlug) => SimpleSlug
ctx.utils.path.transform(from: FullSlug, to: string, opts: TransformOptions) => RelativeURL
ctx.utils.path.toRoot(slug: FullSlug) => RelativeURL
ctx.utils.path.split(slug: FullSlug) => [FullSlug, string]
ctx.utils.path.join(...segments: string[]) => FilePath
Resource Utilities (ctx.utils.resources)
ctx.utils.resources.createExternalJS(src: string, loadTime?: "beforeDOMReady" | "afterDOMReady") => JSResource
ctx.utils.resources.createInlineJS(script: string, loadTime?: "beforeDOMReady" | "afterDOMReady") => JSResource
ctx.utils.resources.createCSS(resource: CSSResource) => CSSResource
Escape Utilities (ctx.utils.escape)
ctx.utils.escape.html(text: string) => string
VFile Data Type Safety
Before: Untyped Data Access
const toc = file.data.toc // Hope this exists!
After: Type-Safe Access
import { QuartzVFileData } from "../vfile-schema"
// TypeScript knows what's available
const toc = file.data.toc // TocEntry[] | undefined (typed!)
Documenting Plugin Data Dependencies
Add JSDoc comments to document what your plugin reads and writes:
/**
* @plugin TableOfContents
* @category Transformer
*
* @reads vfile.data.frontmatter.enableToc
* @writes vfile.data.toc
* @writes vfile.data.collapseToc
*
* @dependencies None
*/
export const TableOfContents: QuartzTransformerPlugin = () => ({
// ...
})
Testing Plugins
Use the new test helpers:
import { describe, it } from "node:test"
import { createMockPluginContext, createMockVFile } from "../test-helpers"
import { TableOfContents } from "./toc"
describe("TableOfContents", () => {
it("should generate TOC from headings", async () => {
const ctx = createMockPluginContext()
const file = createMockVFile({
frontmatter: { title: "Test", enableToc: "true" },
})
const plugin = TableOfContents()
const [markdownPlugin] = plugin.markdownPlugins!(ctx)
// Test the plugin...
})
})
Migration Strategy
- Existing Plugins: No changes required! The old import pattern still works.
- New Plugins: Use
ctx.utilsfor better decoupling and testability. - Gradual Migration: Update plugins incrementally as you work on them.
Important Notes
BuildCtx is Now Readonly
Plugins receive a readonly BuildCtx which prevents mutations:
// ❌ This will cause a TypeScript error
ctx.allSlugs.push(newSlug)
// ✅ Instead, write to vfile.data
file.data.aliases = [newSlug]
Backward Compatibility
All existing plugins continue to work without changes. The new utilities are optional and additive.
Benefits
- Better Testability: Mock utilities easily in tests
- Type Safety: Centralized vfile schema with autocomplete
- Reduced Coupling: Plugins don't import utilities directly
- Clearer Contracts: Document what plugins read/write
- Future-Proof: Easier to version and update utilities
Adding Custom VFile Fields
Custom plugins can add their own fields to the vfile data using TypeScript module augmentation:
import { QuartzTransformerPlugin } from "../types"
export interface MyCustomData {
customField: string
anotherField: number[]
}
/**
* @plugin MyCustomPlugin
* @category Transformer
*
* @writes vfile.data.myCustomData
*/
export const MyCustomPlugin: QuartzTransformerPlugin = () => ({
name: "MyCustomPlugin",
markdownPlugins() {
return [
() => {
return (tree, file) => {
// Add your custom data
file.data.myCustomData = {
customField: "value",
anotherField: [1, 2, 3],
}
}
},
]
},
})
// Extend the VFile DataMap with your custom fields
declare module "vfile" {
interface DataMap {
myCustomData?: MyCustomData
}
}
How it works:
- TypeScript's module augmentation allows multiple
declare module "vfile"statements - Each declaration merges into the same
DataMapinterface - Your custom fields become type-safe alongside built-in fields
- The centralized
vfile-schema.tsdoesn't prevent custom extensions
Best practices:
- Export your custom data type interfaces for reuse
- Use optional fields (
?) to indicate data may not always be present - Document what your plugin writes with JSDoc
@writesannotation - Add the module augmentation at the bottom of your plugin file
This allows third-party and custom plugins to extend the vfile data structure without modifying core files.
Next Steps
For more details, see:
quartz/plugins/vfile-schema.ts- VFile data typesquartz/plugins/plugin-context.ts- Plugin utilitiesquartz/plugins/test-helpers.ts- Testing utilitiesDESIGN_DOCUMENT_DECOUPLING.md- Complete strategy document