Skip to content

Conversation

@saberzero1
Copy link
Collaborator

WIP refactor to decouple as much as possible for the upcoming plugin system.

Copilot AI and others added 6 commits November 16, 2025 14:11
* Initial plan

* Add comprehensive plugin decoupling design document

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Address code review feedback: fix incomplete types and missing files

- Add missing transformer files to Appendix A (gfm, linebreaks, oxhugofm, roam)
- Complete emit/partialEmit return types in QuartzEmitterPluginInstance
- Export TocEntry interface in vfile-schema to fix reference issue
- Add missing externalResources method to QuartzTransformerPluginInstance

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Address code review: add imports, fix component example, clarify interfaces, update file count

- Add missing import statements to all code examples (vfile-schema, plugin-context, registry, emitter types, test helpers)
- Fix component example to properly use QuartzComponentConstructor pattern
- Remove redundant externalResources from init() return type to avoid ambiguity
- Add placeholder implementations for helper functions in test example
- Update total file count from ~65 to ~71 to match breakdown (3+13+2+14+30+5+4)

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Address code review: fix type references, add missing properties, improve examples

- Fix JSResourceOptions/CSSResourceOptions to use actual JSResource/CSSResource types
- Add missing externalResources property to QuartzEmitterPluginInstance
- Clarify that requiredComponents is only for emitters, not transformers
- Fix TOC slug comment to indicate no "#" prefix (e.g., "some-heading")
- Expand frontmatter interface to include all fields from actual implementation
- Improve migration guide with complete example showing type import pattern
- Add skeleton implementations for test helper mock functions with realistic return types

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Address code review: fix imports, add required fields, simplify types

- Fix QuartzVFileData import from "./vfile-schema" instead of "./plugin-context"
- Add all missing import statements for types used in test helpers
- Add required config fields (enableSPA, enablePopovers, analytics, ignorePatterns, defaultDateType, theme)
- Import TocEntry type alongside QuartzVFileData in migration guide
- Simplify resource creation to separate createExternalJS/createInlineJS functions
- Remove circular import of ChangeEvent from "./types" (already defined in that file)
- Fix enableToc type to boolean | string for consistency with other frontmatter fields
- Update mock utilities to match new resource creation pattern

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>
* 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>
* Initial plan

* Extend PluginUtilities with missing path and escape utilities

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Migrate transformers to use ctx.utils instead of direct imports

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Migrate emitters to use ctx.utils - part 1 (helpers, aliases, assets, contentIndex, contentPage, tagPage, static, favicon)

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Complete emitter migration to ctx.utils - all emitters decoupled

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Add comment explaining double-unescaping in test helper mock

This mirrors the production implementation in util/escape.ts which has the same pattern. The CodeQL alert is a known limitation of simple HTML unescaping that exists in the production code as well.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Fix type safety: change join() return type to string and simplify casts

- Changed PluginUtilities.path.join() return type from FilePath to string to allow proper single-cast at call sites
- Removed unsafe double-casts (as unknown as FullSlug) in favor of single casts (as FullSlug)
- Fixed ogImage.tsx to use consistent destructuring pattern (ctx.cfg.configuration instead of cfg.configuration)
- Updated implementation in plugin-context.ts and test-helpers.ts

This addresses all code review feedback about type safety issues.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Fix test mock implementations to match production code behavior

- Fixed getAllSegmentPrefixes to return cumulative prefixes (e.g., ["a", "a/b", "a/b/c"])
- Fixed isRelativeURL to include all three validation conditions
- Fixed stripSlashes to use substring(1) instead of replace(/^\/+/)
- Fixed slugTag to properly handle hierarchical tags with sluggify logic
- Fixed split to handle PDF files and anchor normalization
- Fixed HTML entity &jackyzha0#39; to &jackyzha0#39; for consistency
- Changed QUARTZ imports to use utils.path.QUARTZ for consistency
- Fixed favicon.ts to pass full ctx instead of reconstructing partial object

All mocks now accurately reflect production code behavior for reliable testing.

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>
)

* Initial plan

* Break component-emitter coupling by introducing shared-types module

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Decouple transformer from component scripts via resource registry

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Document plugin data dependencies with @plugin, @reads, @writes annotations

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Address code review feedback: improve docs and exhaustiveness checking

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Add exhaustiveness checking with unreachable assertions

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Fix getComponentJS return type and remove unnecessary null checks

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)

* Initial plan

* refactor: remove BuildCtx mutation from FrontMatter plugin

- Remove temporary cast to mutable allSlugs array
- Move alias collection to build orchestration layer
- Update ctx.allSlugs immutably after parsing
- Apply same pattern to incremental rebuild
- Verified alias functionality works correctly

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* fix: ensure alias collection happens before filtering in rebuild flow

Move alias collection before filterContent() in rebuild flow to match
initial build flow. This ensures consistent behavior where aliases from
all markdown files (including those that will be filtered out) are
included in ctx.allSlugs in both build scenarios.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* refactor: simplify collectAliases using functional array methods

Replace imperative for-loop with declarative filter/flatMap chain for
better readability and conciseness. Functionally equivalent but more
idiomatic TypeScript.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* docs: update roadmap with completion status for decoupling phases

Mark phases 1-5 as completed with detailed status notes:
- Phase 1 (Foundation): vfile-schema, plugin-context, test-helpers
- Phase 2 (Utility Abstraction): ctx.utils migration complete
- Phase 3 (Component Decoupling): component registry created
- Phase 4 (Immutability): BuildCtx readonly, alias collection refactored
- Phase 5 (Full Migration): all plugins migrated to new pattern

Add implementation status summary showing all objectives achieved.

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>
* Initial plan

* Initial analysis and plan for decoupling completion

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* docs: add @plugin annotations to transformers missing documentation

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* docs: mark decoupling phases and success criteria as complete

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* fix: move @plugin annotation in roam.ts to correct location

Move the @plugin documentation block to immediately precede the
RoamFlavoredMarkdown export, consistent with other transformer files
(gfm.ts, syntax.ts, linebreaks.ts). Previously it was placed before
the regex constant declarations.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* Changes before error encountered

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* refactor: move documentation files from docs/ to project root

Move IMPLEMENTATION_SUMMARY.md, PLUGIN_MIGRATION.md, and SECURITY_SUMMARY.md
from docs/ directory to project root to keep them separate from user-facing
documentation.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* config: add implementation docs to ignore patterns

Add IMPLEMENTATION_SUMMARY.md, PLUGIN_MIGRATION.md, and SECURITY_SUMMARY.md
to ignorePatterns in quartz.config.ts to exclude them from the documentation
build. These files are implementation documentation for the project itself,
not user-facing documentation.

Co-authored-by: saberzero1 <8161064+saberzero1@users.noreply.github.com>

* chore: remove build output directories from git tracking

Remove public-current and public-v4 directories that were accidentally
committed during build testing. These directories are already covered by
.gitignore and should not be tracked in the repository.

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>
@github-actions
Copy link

github-actions bot commented Nov 17, 2025

built with Refined Cloudflare Pages Action

⚡ Cloudflare Pages Deployment

Name Status Preview Last Commit
quartz ✅ Ready (View Log) Visit Preview 2b63a09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR implements a comprehensive refactoring to decouple the Quartz build pipeline, improving modularity and maintainability. The changes introduce a plugin utilities abstraction layer, centralize VFile data types, and enforce immutability in the build context to prevent unintended side effects.

Key changes:

  • Introduces PluginUtilities interface for abstracted utility functions accessible via ctx.utils
  • Centralizes VFile data schema in vfile-schema.ts for type safety
  • Makes BuildCtx readonly with a separate MutableBuildCtx for orchestration

Reviewed Changes

Copilot reviewed 46 out of 46 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
quartz/util/path.ts Updated TransformOptions.allSlugs to readonly array
quartz/util/ctx.ts Added readonly modifiers to BuildCtx and created MutableBuildCtx
quartz/plugins/vfile-schema.ts New centralized VFile data type definitions
quartz/plugins/plugin-context.ts New plugin utilities abstraction layer
quartz/plugins/test-helpers.ts New testing utilities for plugin development
quartz/plugins/shared-types.ts New shared type definitions breaking circular dependencies
quartz/components/resources.ts New component resource registry
quartz/plugins/transformers/*.ts Updated to use ctx.utils and added documentation
quartz/plugins/emitters/*.ts Updated to use ctx.utils instead of direct imports
quartz/plugins/filters/*.ts Added JSDoc documentation
quartz/components/*.tsx Removed mutations of ctx.trie
quartz/build.ts Updated to use MutableBuildCtx and collect aliases properly
quartz.config.ts Added documentation files to ignore patterns
Documentation files New migration guide, security summary, and implementation summary

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +82
split: (slug: string) => {
const [path, anchor] = splitAnchor(slug)
return [path, anchor]
},
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The split function wrapper adds no value - it simply calls splitAnchor and returns its result. Consider removing this wrapper and directly assigning splitAnchor to maintain simplicity: split: splitAnchor.

Suggested change
split: (slug: string) => {
const [path, anchor] = splitAnchor(slug)
return [path, anchor]
},
split: splitAnchor,
Copilot uses AI. Check for mistakes.
Comment on lines +192 to +198
unescape: (html: string) =>
html
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
.replace(/&quot;/g, '"')
.replace(/&#039;/g, "'"),
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unescape implementation has a double-unescaping vulnerability. If input contains &amp;amp;, it will be converted to &amp; (correct), but then in a second call, that &amp; will be converted to & (incorrect). The comment acknowledges this matches production, but this is a security issue that should be fixed in both the mock and production code. Consider implementing proper HTML entity decoding that handles all cases correctly, or at minimum, add a TODO comment indicating this is a known issue that needs fixing.

Copilot uses AI. Check for mistakes.

const tags = coerceToArray(coalesceAliases(data, ["tags", "tag"]))
if (tags) data.tags = [...new Set(tags.map((tag: string) => slugTag(tag)))]
if (tags) data.tags = [...new Set(tags.map((tag: string) => utils!.path.slugTag(tag)))]
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-null assertion utils! is used throughout this file. While the code works, this bypasses TypeScript's safety. Consider adding a guard at the function start: if (!utils) throw new Error('utils not available') or restructure the code to handle the optional utils gracefully.

Copilot uses AI. Check for mistakes.
Comment on lines +52 to +55
.filter(([_, file]) => file.data.aliases)
.flatMap(([_, file]) => file.data.aliases!)
}

Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function uses a non-null assertion file.data.aliases! after filtering for its existence. While safe due to the filter, this could be more explicit: .flatMap(([_, file]) => file.data.aliases ?? []) to avoid the assertion and handle edge cases more robustly.

Suggested change
.filter(([_, file]) => file.data.aliases)
.flatMap(([_, file]) => file.data.aliases!)
}
.flatMap(([_, file]) => file.data.aliases ?? [])
}
Copilot uses AI. Check for mistakes.
Comment on lines +88 to +90
utils!.escape.unescape(
fileData.description?.trim() ?? i18n(cfg.locale).propertyDefaults.description,
)
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Multiple non-null assertions on utils! throughout emitter files create potential runtime errors if utils is undefined. Consider adding type guards or making utils required in the BuildCtx interface for emitters, as they consistently need these utilities.

Copilot uses AI. Check for mistakes.
const { tree, fileData, allFiles, cfg } = props

const trie = (props.ctx.trie ??= trieFromAllFiles(allFiles))
const trie = props.ctx.trie ?? trieFromAllFiles(allFiles)
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The trie is now computed on every render if ctx.trie is undefined. Previously, with the nullish coalescing assignment operator, it was computed once and cached. Consider memoizing this computation with useMemo to avoid unnecessary recalculations on re-renders.

Copilot uses AI. Check for mistakes.
ctx,
}: QuartzComponentProps) => {
const trie = (ctx.trie ??= trieFromAllFiles(allFiles))
const trie = ctx.trie ?? trieFromAllFiles(allFiles)
Copy link

Copilot AI Nov 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to FolderContent, the trie computation is no longer cached. This could impact performance if the component re-renders frequently. Consider using useMemo to cache the trie computation: const trie = useMemo(() => ctx.trie ?? trieFromAllFiles(allFiles), [ctx.trie, allFiles]).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant