Skip to content

merge: sync with upstream jackyzha0/quartz v5 (23 commits) - #18

Merged
Faustze merged 25 commits into
mainfrom
upstream-sync-v5
Jul 23, 2026
Merged

merge: sync with upstream jackyzha0/quartz v5 (23 commits)#18
Faustze merged 25 commits into
mainfrom
upstream-sync-v5

Conversation

@Faustze

@Faustze Faustze commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Summary

Merges 23 commits from upstream/v5 (jackyzha0/quartz). We were 22-23 commits behind before this.

Main upstream changes:

  • npm-specifier support for @quartz-community/* plugins, as an alternative to github: git-source specifiers
  • fix: decouple Head.tsx from og-image plugin import (#2495)
  • assorted docs/style fixes and a type-safe plugin index generator

Decision: migrated our config to npm specifiers

All 44 first-party plugins in quartz.config.yaml / quartz.config.default.yaml were switched from github:quartz-community/* to @quartz-community/* npm specifiers. Our one local third-party plugin (./local-plugins/lang-switch) is untouched — github: sources remain fully supported.

This removes the git-clone + tsup build step for these plugins during CI. That's the same code path that caused this morning's flaky favicon/og-image build failure (sh: 1: tsup: not found) which I patched with a retry loop — npm packages now ship pre-built, so that whole class of flakiness goes away. Locally, the "Install Quartz plugins" step dropped from ~7 minutes to under 1 second.

Conflict resolutions

  • package.json: kept our newer dependency versions (post dependabot merges), added upstream's ~44 @quartz-community/* deps and hast-util-from-html (already imported directly in dispatcher.ts but missing from the manifest)
  • package-lock.json: regenerated via npm install, then re-applied the immutable/brace-expansion security bumps from the prior security PR
  • quartz/plugins/loader/install-plugins.ts: took upstream's version — it handles both git- and npm-sourced plugins (strict superset of ours)
  • quartz.lock.json: deleted (upstream removed it; no longer needed once plugins are npm-installed rather than git-cloned)

Test plan

  • npx tsc --noEmit — clean
  • npx prettier . --check — clean (aside from pre-existing unrelated content-ru/ drift, not part of this branch)
  • npx tsx --test — 109/109 passing
  • npx tsx ./quartz/plugins/loader/install-plugins.ts — all 36 plugins installed correctly, <1s
  • npx quartz build — succeeds, 339 files emitted (same as before merge)
  • npm ci (matching our deploy workflow) — succeeds with the regenerated lockfile
  • npm audit — 0 vulnerabilities
  • Verified lang-switch (local plugin) still renders in output HTML

Summary by CodeRabbit

  • New Features

    • Community plugins can now be added using npm package specifiers such as @quartz-community/....
    • Plugin installation and loading now support both npm packages and existing GitHub-based sources.
    • Updated project templates use the new npm-based plugin format.
  • Documentation

    • Added migration guidance for switching plugin references to npm specifiers.
    • GitHub-based plugin references remain supported for compatibility.
  • Bug Fixes

    • Improved plugin discovery and loading for npm-installed packages.

saberzero1 and others added 24 commits July 23, 2026 01:17
Plugins can now be referenced as "@quartz-community/<name>" in
quartz.config.yaml and via 'quartz plugin add'. The npm path skips
git installation and resolves from node_modules.

Changes:
- gitLoader.ts: detect @scope/name as npm package in parsePluginSource()
- config-loader.ts: skip git install for npm packages, read manifests
  from node_modules via createRequire
- install-plugins.ts: filter npm packages from prebuild, fallback to
  YAML config parsing to avoid loading full quartz.ts
- plugin-data.js: npm detection in CLI parseGitSource()
- plugin-git-handlers.js: npm install path in handlePluginAdd()
- package.json: add missing hast-util-from-html dependency
- quartz.config.default.yaml: all 44 plugins use @quartz-community/* npm specifiers
- cli/templates/*.yaml: all 4 templates updated (default, blog, obsidian, ttrpg)
- bootstrap-cli.mjs: updated help text
- package.json: devDeps use npm semver ranges instead of github: specifiers
- gitLoader.ts: regeneratePluginIndex scans npm packages from node_modules
- install-plugins.ts: generates plugin index for npm packages
- Removed quartz.lock.json (only relevant for git-installed plugins)
With npm specifiers, quartz.lock.json may not exist.
The glob pattern makes the COPY conditional.
- Added all 44 default plugins as dependencies so npm ci installs them
- Fixed @quartz-community/fonts → @quartz-community/quartz-fonts (actual npm name)
- Regenerated package-lock.json with plugin dependencies
og-image requires sharp as a peer dep which conflicts with the
version installed by quartz core. legacy-peer-deps allows both
to coexist until the peer dep ranges are aligned.
The lockfile was generated on linux x86_64 but CI needs platform-
specific optional deps for all architectures. npm install resolves
them correctly while npm ci requires exact lockfile match.
The npm specifier path requires additional work in the esbuild
transpilation pipeline to mark npm imports as external. Reverting
to github: specifiers to restore the docs site immediately.

The npm loader code (parsePluginSource, config-loader, CLI) remains
in place for future use. Only the default config/templates are reverted.
- Default config and all 4 templates use @quartz-community/* npm specifiers
- config-loader: npm packages imported directly (not via .quartz/plugins/ paths)
- gitLoader: regeneratePluginIndex cross-references .d.ts with .js to correctly
  classify type-only exports, preventing runtime 'does not provide export' errors
- All 44 default plugins added as dependencies
- Locally tested: 111 docs files → 374 output files, zero errors
…er ranges)

Multiple plugins declare incompatible peer dep ranges for shared packages
(sharp, @myriaddreamin/rehype-typst, etc). These are optional peers that
work correctly at runtime. legacy-peer-deps is the appropriate setting
for a host application consuming many plugins.
Remove unconditional import of CustomOgImagesEmitterName from
.quartz/plugins in Head.tsx. This caused builds to fail when the
og-image plugin was not listed in quartz.config.yaml, violating the
plugin system's opt-in contract.

The fix inlines the emitter name string constant at the usage site.

Also convert the ContentDetails import in fileTrie.ts to
`import type` since it is only used in a type position.
Brings in npm-specifier support for quartz-community plugins,
alongside upstream fixes (og-image/Head.tsx decoupling, docs).

Conflict resolutions:
- package.json: kept our newer dependency versions (post dependabot
  merges), added upstream's ~44 @quartz-community/* npm deps and
  hast-util-from-html (already imported directly in dispatcher.ts
  but missing from the manifest)
- package-lock.json: regenerated via `npm install`, then re-applied
  the immutable/brace-expansion security bumps from the prior PR
- quartz/plugins/loader/install-plugins.ts: took upstream's version,
  which handles both git- and npm-sourced plugins (superset of ours)
- quartz.lock.json: deleted (upstream removed it; no longer needed
  once plugins are npm-installed rather than git-cloned)

Migrated our quartz.config.yaml and quartz.config.default.yaml from
github: specifiers to @quartz-community/* npm specifiers for all 44
first-party plugins (github: remains supported for the one local
third-party plugin, ./local-plugins/lang-switch, untouched). This
eliminates the git-clone + tsup build step for these plugins during
CI, removing the class of flakiness fixed earlier today (favicon/
og-image "tsup: not found") since npm packages ship pre-built.

Verified: tsc --noEmit clean, prettier clean, 109/109 tests passing,
full quartz build succeeds (339 files), plugin install step dropped
from ~7 minutes to under 1 second.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@Faustze, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 47 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 580fb7df-1387-4fa4-a752-fef978f64b8a

📥 Commits

Reviewing files that changed from the base of the PR and between e055ead and a0851cb.

📒 Files selected for processing (1)
  • quartz/cli/plugin-git-handlers.js
📝 Walkthrough

Walkthrough

Quartz migrates community plugin references from GitHub sources to scoped npm packages, adds npm plugin parsing and loading, updates configuration templates and dependencies, and changes CI, deployment, and Docker installs to use npm install.

Changes

npm Plugin Migration

Layer / File(s) Summary
Scoped source parsing and CLI installation
quartz/cli/plugin-data.js, quartz/cli/plugin-git-handlers.js, quartz/plugins/loader/gitLoader.ts
Scoped npm package sources are parsed, installed, and represented with npm-specific plugin metadata.
npm plugin loading and index generation
quartz/plugins/loader/config-loader.ts, quartz/plugins/loader/gitLoader.ts, quartz/plugins/loader/install-plugins.ts
Plugin orchestration separates Git and npm sources; npm packages are resolved, indexed, and imported by package name.
npm package declarations and plugin configuration
package.json, quartz.config*.yaml, quartz/cli/templates/*.yaml, docs/getting-started/upgrading.md, quartz/bootstrap-cli.mjs, quartz/plugins/loader/types.ts, quartz.lock.json
Dependencies, configuration sources, templates, documentation, and examples are updated for scoped npm plugins; the Quartz lockfile is removed.
Install environment and runtime cleanup
.github/workflows/*.yaml, .npmrc, Dockerfile, quartz/components/Head.tsx, quartz/util/fileTrie.ts
Automated and Docker installs use npm install, legacy peer dependency handling is enabled, and two imports are adjusted.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant QuartzCLI
  participant npm
  participant PluginLoader
  participant QuartzConfig
  User->>QuartzCLI: Add or configure `@quartz-community/plugin`
  QuartzCLI->>npm: Install scoped package
  PluginLoader->>npm: Resolve package metadata and exports
  PluginLoader->>QuartzConfig: Generate plugin index
  QuartzConfig->>PluginLoader: Import npm plugin by package name
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately reflects the main change: syncing Quartz with upstream v5.
Description check ✅ Passed The description is detailed and covers summary, conflict resolutions, and test plan, matching the template well.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch upstream-sync-v5

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread quartz/cli/plugin-git-handlers.js Fixed
CodeQL flagged js/shell-command-injection-from-environment on the
`quartz plugin add <npm-package>` path merged from upstream: it built
a shell string from a user-supplied plugin name and ran it via
execSync. Switch to execFileSync with an argv array so the plugin
name is never interpreted by a shell.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (1)
quartz/plugins/loader/gitLoader.ts (1)

982-1074: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Duplicate export-discovery logic between the Git and npm loops.

Lines 982–1016 (Git dirs) and 1038–1074 (npm packages) are near-identical: .d.ts parsing, dist/index.js export scan, named/extraTypes/types computation, and overridable/passthrough classification. Extract a shared helper (taking distIndex, jsIndex, and a key/importPath) to avoid divergence as this logic evolves.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/plugins/loader/gitLoader.ts` around lines 982 - 1074, Extract the
duplicated export-discovery and classification flow from the Git and npm loops
into a shared helper that accepts the declaration path, JavaScript index path,
and plugin key/import path. Have the helper perform d.ts parsing, JavaScript
export scanning, type computation, and overridable/passthrough classification,
then update both loops to reuse it while preserving their existing registration
and nameCount behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Dockerfile`:
- Line 11: Remove the `COPY quartz.lock.json* .` instruction from the Dockerfile
so the build no longer references the deleted lockfile and can proceed to `npm
install`.

In `@quartz/bootstrap-cli.mjs`:
- Line 43: Update launchTui and the TUI installation guidance together: resolve
the runtime entry point from the installed `@quartz-community/tui` package under
node_modules, or create a compatible .quartz/plugins/tui path, so the advertised
npm command produces a path launchTui can find. Ensure the missing-plugin check
and execution path use the same resolved location before retaining this message.

In `@quartz/cli/plugin-git-handlers.js`:
- Around line 1193-1200: Prevent shell injection across the npm source flow: in
quartz/cli/plugin-git-handlers.js at lines 1193-1200, replace the interpolated
execSync npm installation with spawnSync or execFile using separate command
arguments and preserve error handling; in quartz/cli/plugin-data.js at lines
193-197, validate npm package specifiers before classifying them as npm sources,
rejecting shell metacharacters or otherwise invalid package names.

In `@quartz/cli/templates/obsidian.yaml`:
- Line 271: Update the Excalidraw plugin entry in the Obsidian template to use
the GitHub repository specifier for quartz-community/obsidian-plugin-excalidraw
instead of the `@quartz-community` npm package name. Preserve the existing source
entry structure and align it with the quartz-themes block and documented quartz
plugin add command.

In `@quartz/plugins/loader/config-loader.ts`:
- Line 450: Update the plugin name assignment in the config loader to use
spec.name directly, removing the redundant npmPackage ternary while preserving
the resulting value.

In `@quartz/plugins/loader/gitLoader.ts`:
- Around line 1018-1029: Update the npm package manifest resolution in the
npmPackages loop to use createRequire with the project root search path, passing
{ paths: [process.cwd()] } when resolving `${npmPkg}/package.json`. Keep the
existing dist/index.d.ts construction and missing-package handling unchanged,
matching the resolution approach used by config-loader.ts.

---

Nitpick comments:
In `@quartz/plugins/loader/gitLoader.ts`:
- Around line 982-1074: Extract the duplicated export-discovery and
classification flow from the Git and npm loops into a shared helper that accepts
the declaration path, JavaScript index path, and plugin key/import path. Have
the helper perform d.ts parsing, JavaScript export scanning, type computation,
and overridable/passthrough classification, then update both loops to reuse it
while preserving their existing registration and nameCount behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9d5bd35c-ac5d-4b32-b3df-aa100117551f

📥 Commits

Reviewing files that changed from the base of the PR and between 65d3b86 and e055ead.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • .github/workflows/ci.yaml
  • .github/workflows/templates/build-preview.yaml
  • .github/workflows/templates/deploy-v5.yaml
  • .npmrc
  • Dockerfile
  • docs/getting-started/upgrading.md
  • package.json
  • quartz.config.default.yaml
  • quartz.config.yaml
  • quartz.lock.json
  • quartz/bootstrap-cli.mjs
  • quartz/cli/plugin-data.js
  • quartz/cli/plugin-git-handlers.js
  • quartz/cli/templates/blog.yaml
  • quartz/cli/templates/default.yaml
  • quartz/cli/templates/obsidian.yaml
  • quartz/cli/templates/ttrpg.yaml
  • quartz/components/Head.tsx
  • quartz/plugins/loader/config-loader.ts
  • quartz/plugins/loader/gitLoader.ts
  • quartz/plugins/loader/install-plugins.ts
  • quartz/plugins/loader/types.ts
  • quartz/util/fileTrie.ts
💤 Files with no reviewable changes (1)
  • quartz.lock.json

Comment thread Dockerfile
COPY quartz/ ./quartz/
COPY quartz.lock.json .
RUN npm ci; npx quartz plugin install
COPY quartz.lock.json* .

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Remove the deleted lockfile from the Docker copy step.

quartz.lock.json is removed in this PR, so COPY quartz.lock.json* . has no source match and the image build fails before npm install. Remove this instruction.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Dockerfile` at line 11, Remove the `COPY quartz.lock.json* .` instruction
from the Dockerfile so the build no longer references the deleted lockfile and
can proceed to `npm install`.

Comment thread quartz/bootstrap-cli.mjs
console.error(
"TUI plugin not installed. Install with:\n" +
" npx quartz plugin add github:quartz-community/tui\n",
" npx quartz plugin add @quartz-community/tui\n",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the TUI runtime path before advertising npm installation.

The instructed command installs under node_modules/@quartz-community/tui, while launchTui still checks .quartz/plugins/tui/dist/App.mjs. A newly installed npm TUI will therefore still be reported as missing. Resolve the installed package path (or preserve a compatible link) before updating this message.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/bootstrap-cli.mjs` at line 43, Update launchTui and the TUI
installation guidance together: resolve the runtime entry point from the
installed `@quartz-community/tui` package under node_modules, or create a
compatible .quartz/plugins/tui path, so the advertised npm command produces a
path launchTui can find. Ensure the missing-plugin check and execution path use
the same resolved location before retaining this message.

Comment on lines +1193 to +1200
if (parsed.npmPackage) {
const name = nameOverride ?? parsed.name
console.log(styleText("cyan", `→ Installing ${name} from npm...`))
execSync(`npm install ${parsed.name}`, { cwd: process.cwd(), stdio: "inherit" })
const configSource = nameOverride ? { repo: parsed.name, name: nameOverride } : parsed.name
const pluginDir = path.join(process.cwd(), "node_modules", ...parsed.name.split("/"))
addedPlugins.push({ name, pluginDir, source: parsed.name, configSource })
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C 3 'execSync\(`npm install \$\{parsed\.name\}' quartz/cli/plugin-git-handlers.js
rg -n -C 3 'startsWith\("@".*includes\("/"' quartz/cli/plugin-data.js

Repository: Faustze/notes

Length of output: 1053


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate files =="
git ls-files | rg '(^|/)plugin-(git-handlers|data)\.js$' || true

echo
echo "== plugin-git-handlers relevant section =="
sed -n '1160,1215p' quartz/cli/plugin-git-handlers.js | cat -n

echo
echo "== plugin-data relevant section =="
sed -n '170,210p' quartz/cli/plugin-data.js | cat -n

echo
echo "== parsePluginSource imports/top =="
sed -n '1,80p' quartz/cli/plugin-data.js | cat -n

echo
echo "== call sites for parsePluginSource / npmPackage handling =="
rg -n "parsePluginSource|npmPackage|pluginDir|configSource|source:" quartz/cli/plugin-git-handlers.js quartz/cli/plugin-data.js

Repository: Faustze/notes

Length of output: 24083


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

git = Path("quartz/cli/plugin-git-handlers.js").read_text()
data = Path("quartz/cli/plugin-data.js").read_text()

print("plugin-git-handlers execSync npm line:", bool(re.search(r'execSync\(`npm install \$\{parsed\.name\}', git)))
print("plugin-data npm package branch:", bool("# Handle npm scoped packages" in data and 'url.startsWith("@")' in data and 'url.includes("/")' in data and "!url.includes(":")) in data))

inputs = [
    "`@scope/pkg`",
    "`@scope/pkg`; echo PWNED >/tmp/pwned",
    "`@scope/`@nested/pkg",
    "`@scope/pkg`@1.0.0; echo PWNED >/tmp/pwned2",
    "`@scope/pkg`#subdir; echo PWNED >/tmp/pwned3",
]

for value in inputs:
    npm_package = (isinstance("", str) and value.startswith("@") and "/" in value and ":" not in value) and True
    # approximate template used by code for npm install command
    cmd = f"npm install {value}"
    print(f"input={value!r} npmPackage={npm_package} command={cmd!r};shell_meta_chars={set(";!|&$`").intersection(value)}")

python_path_expr = re.compile(r'node_modules(?:/.*?)*package\.json', re.S)
match = python_path_expr.search("node_modules/@scope/package-name/package.json")
print("python path traversal safe for `@scope/package-name/package.json`:", bool(match and ".." not in match.group(0)), "path:", match.group(0) if match else None)
PY

echo
echo "== package manager config =="
for f in package.json package-lock.json pnpm-lock.yaml yarn.lock bun.lockb; do
  [ -f "$f" ] && echo "--- $f" && (if [ "$f" = package.json ]; then node -e "const p=require('./package.json'); console.log(JSON.stringify({engines:p.engines,workspaces:p.workspaces},null,2))"; fi)
done

Repository: Faustze/notes

Length of output: 571


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

git = Path("quartz/cli/plugin-git-handlers.js").read_text()
data = Path("quartz/cli/plugin-data.js").read_text()

print("plugin-git-handlers execSync npm line:", bool(re.search(r'execSync\(`npm install \$\{parsed\.name\}', git)))
print("plugin-data npm package branch:", bool("# Handle npm scoped packages" in data and 'url.startsWith("@")' in data and 'url.includes("/")' in data and '!url.includes(":")' in data))

inputs = [
    "`@scope/pkg`",
    "`@scope/pkg`; echo PWNED >/tmp/pwned",
    "`@scope/`@nested/pkg",
    "`@scope/pkg`@1.0.0; echo PWNED >/tmp/pwned2",
    "`@scope/pkg`#subdir; echo PWNED >/tmp/pwned3",
]

for value in inputs:
    npm_package = value.startswith("@") and "/" in value and ":" not in value
    cmd = f"npm install {value}"
    print(f"input={value!r} npmPackage={npm_package} command={cmd!r};shell_meta_chars={set(';!|&$`').intersection(value)}")

match = Path('quartz/cli/plugin-git-handlers.js').read_text()  # no-op import if needed; original path was Python expression, not repo
python_path_expr = re.compile(r'node_modules(?:/.*?)*package\.json', re.S)
match = python_path_expr.search("node_modules/@scope/package-name/package.json")
print("package.json path for `@scope/package-name`:", match.group(0) if match else None)
PY

echo
echo "== package manager config =="
if [ -f package.json ]; then
  echo "--- package.json"
  node -e "const p=require('./package.json'); console.log(JSON.stringify({engines:p.engines,workspaces:p.workspaces,scripts:p.scripts},null,2))"
fi
for f in package-lock.json pnpm-lock.yaml yarn.lock; do
  [ -f "$f" ] && echo "--- $f (first 5 lines):" && head -5 "$f"
done

Repository: Faustze/notes

Length of output: 2120


Prevent command injection in npm plugin installation.

quartz/cli/plugin-data.js classifies any @.../... without : as an npm source, and quartz/cli/plugin-git-handlers.js passes that CLI-controlled name directly into execSync("npm install ${parsed.name}"), so inputs like @scope/pkg; ... can execute arbitrary shell commands. Install through child_process.spawnSync/execFile with an argument array, and validate npm package specifiers before treating them as npm sources.

🧰 Tools
🪛 GitHub Check: CodeQL

[warning] 1196-1196: Shell command built from environment values
This shell command depends on an uncontrolled absolute path.

🪛 OpenGrep (1.25.0)

[ERROR] 1196-1196: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

📍 Affects 2 files
  • quartz/cli/plugin-git-handlers.js#L1193-L1200 (this comment)
  • quartz/cli/plugin-data.js#L193-L197
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/cli/plugin-git-handlers.js` around lines 1193 - 1200, Prevent shell
injection across the npm source flow: in quartz/cli/plugin-git-handlers.js at
lines 1193-1200, replace the interpolated execSync npm installation with
spawnSync or execFile using separate command arguments and preserve error
handling; in quartz/cli/plugin-data.js at lines 193-197, validate npm package
specifiers before classifying them as npm sources, rejecting shell
metacharacters or otherwise invalid package names.

Source: Linters/SAST tools

options:
theme: default
- source: github:quartz-community/obsidian-plugin-excalidraw
- source: "@quartz-community/obsidian-plugin-excalidraw"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
npm view `@quartz-community/obsidian-plugin-excalidraw` version 2>/dev/null || echo "MISSING on npm"

Repository: Faustze/notes

Length of output: 143


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate obsidian.yaml =="
git ls-files | grep -F 'obsidian.yaml' || true

echo "== package manager files =="
git ls-files | grep -E '(^|/)(package.json|pnpm-lock.yaml|package-lock.json|yarn.lock)$' || true

echo "== relevant package.json deps/search =="
for f in $(git ls-files | grep -E '(^|/)package.json$'); do
  echo "--- $f"
  python3 - <<'PY' "$f"
import json, sys
p=sys.argv[1]
try:
    data=json.load(open(p))
except json.JSONDecodeError as e:
    print("JSON_ERROR", e)
    raise SystemExit
deps={}
for k in ("dependencies","devDependencies","peerDependencies","optionalDependencies"):
    deps.update(data.get(k) or {})
for name in ("`@quartz-community/obsidian-plugin-excalidraw`", "obsidian-plugin-excalidraw", "quartz-themes"):
    print(name, deps.get(name))
PY
done

echo "== template occurrences =="
rg -n "`@quartz-community/obsidian-plugin-excalidraw`|obsidian-plugin-excalidraw|quartz-themes|source:" quartz/cli/templates/obsidian.yaml package.json 2>/dev/null || true

Repository: Faustze/notes

Length of output: 4597


🌐 Web query:

npm package @quartz-community/obsidian-plugin-excalidraw``

💡 Result:

The @quartz-community/obsidian-plugin-excalidraw is a Quartz community plugin designed to render Obsidian Excalidraw drawings as full-page, interactive SVG visualizations within a Quartz site [1][2]. Despite the name format suggesting it might be an npm package, it is primarily distributed and installed directly via GitHub within the Quartz ecosystem [1]. You do not install it via the standard npm install command; instead, you add it to your Quartz project using the Quartz CLI [1][2]: npx quartz plugin add github:quartz-community/obsidian-plugin-excalidraw Key details about the plugin include: Functionality: It parses and renders both.excalidraw.md and.excalidraw files, supporting features like shapes, text, arrows, embedded notes (transcluded content), and embedded webpages via iframes [1][3]. It also handles dark/light theme adaptation automatically [1]. Compatibility: It is designed for use with Quartz (specifically v5) and serves as the bridge for Obsidian users to display their Excalidraw content online [4][3][5]. Configuration: Once added, it can be configured in your quartz.config.yaml file to adjust settings such as enableInteraction, darkMode, and exportPadding [1][2]. For further details or to report issues, the project is hosted on GitHub under the quartz-community organization [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant obsidian.yaml lines =="
sed -n '258,274p' quartz/cli/templates/obsidian.yaml | cat -n

echo "== fetch release/dist info for quartz-community/excalidraw package if available =="
python3 - <<'PY'
import json, urllib.request
packages = [
    "https://registry.npmjs.org/@quartz-community/obsidian-plugin-excalidraw",
    "https://api.github.com/repos/quartz-community/obsidian-plugin-excalidraw",
]
for url in packages:
    print(f"--- {url}")
    try:
        req = urllib.request.Request(url, headers={"Accept": "application/json", "User-Agent": "CodeRabbit"})
        with urllib.request.urlopen(req, timeout=20) as resp:
            data = json.loads(resp.read())
        if url.startswith("https://api.github.com"):
            print("name", data.get("name"))
            print("default_branch", data.get("default_branch"))
            print("has_package_json", ".github/workflows/npm-publish.yml" in data.get("archive_submodule", "") or any("npm" in (x.get("path") or "").lower() for x in data.get("files", [])))
        else:
            print("latest_version", data.get("dist-tags", {}).get("latest"))
            print("published", bool(data.get("time", {}).get("modified") == data.get("time", {}).get("created", {}).get("modified")))
            print("keywords", data.get("keywords"))
    except Exception as e:
        print(type(e).__name__, str(e))
PY

Repository: Faustze/notes

Length of output: 1215


🌐 Web query:

github quartz-community obsidian-plugin-excalidraw package.json

💡 Result:

The query involves two distinct GitHub repositories related to Excalidraw and Obsidian: 1. quartz-community/obsidian-plugin-excalidraw: This is a Quartz plugin designed to render Obsidian Excalidraw drawings as interactive SVG visualizations within a Quartz-generated site [1][2]. It is installed into a Quartz project via npx quartz plugin add github:quartz-community/obsidian-plugin-excalidraw [1][3]. The repository documentation indicates it is based on the quartz-community/plugin-template [1]. 2. zsviczian/obsidian-excalidraw-plugin: This is the primary Obsidian plugin repository for editing and viewing Excalidraw drawings within the Obsidian application [4][5]. Its package.json file serves as a library/package manifest, but it is not the canonical source for the plugin's release version (which is managed in manifest.json and manifest-beta.json) [6][7]. The package.json defines dependencies, scripts for development and building, and library entry points [8]. These two projects serve different environments: the former for static site generation (Quartz) and the latter for the live Obsidian editing environment [1][4].

Citations:


🌐 Web query:

Quartz CLI plugin add npm specifier github:obsidian-plugin-excalidraw

💡 Result:

To add the Quartz integration for the Obsidian Excalidraw plugin to your project, run the following command in your terminal within your Quartz project directory [1][2]: npx quartz plugin add github:quartz-community/obsidian-plugin-excalidraw After running this command, ensure the plugin is enabled in your quartz.config.yaml file [1][3]: plugins: - source: github:quartz-community/obsidian-plugin-excalidraw enabled: true options: enableInteraction: true darkMode: "auto" exportPadding: 20 This plugin allows your Quartz site to render Obsidian Excalidraw drawings (.excalidraw.md and.excalidraw files) as full-page, interactive SVG visualizations [1][4]. If you need to make advanced adjustments, you can also override settings in your quartz.ts file [1][3].

Citations:


Revert Excalidraw back to the GitHub source.

@quartz-community/obsidian-plugin-excalidraw is documented as installed via quartz plugin add github:quartz-community/obsidian-plugin-excalidraw, and this npm specifier can fail during template scaffold if it is not published. Keep the source: entry as a GitHub repo specifier, similar to the quartz-themes block above.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/cli/templates/obsidian.yaml` at line 271, Update the Excalidraw plugin
entry in the Obsidian template to use the GitHub repository specifier for
quartz-community/obsidian-plugin-excalidraw instead of the `@quartz-community` npm
package name. Preserve the existing source entry structure and align it with the
quartz-themes block and documented quartz plugin add command.

const entryPoint = getPluginEntryPoint(spec.name)
module = await import(toFileUrl(entryPoint))
}
const pluginName = spec.npmPackage ? spec.name : spec.name

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Redundant ternary — both branches return spec.name.

spec.npmPackage ? spec.name : spec.name always evaluates to spec.name. Simplify, or confirm the non-npm branch was intended to use a different value (e.g. a normalized/local name).

Proposed fix
-        const pluginName = spec.npmPackage ? spec.name : spec.name
+        const pluginName = spec.name
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const pluginName = spec.npmPackage ? spec.name : spec.name
const pluginName = spec.name
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/plugins/loader/config-loader.ts` at line 450, Update the plugin name
assignment in the config loader to use spec.name directly, removing the
redundant npmPackage ternary while preserving the resulting value.

Comment on lines +1018 to +1029
for (const npmPkg of options.npmPackages ?? []) {
let distIndex: string | undefined
try {
const esmRequire = createRequire(import.meta.url)
const pkgJsonPath = esmRequire.resolve(`${npmPkg}/package.json`)
distIndex = path.join(path.dirname(pkgJsonPath), "dist", "index.d.ts")
} catch {
if (options.verbose) {
console.log(styleText("yellow", `⚠`), `Skipping npm package ${npmPkg}: not found`)
}
continue
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare npm resolution options across call sites
rg -nP "resolve\(\`\\$\{.*\}/package\.json\`" quartz

Repository: Faustze/notes

Length of output: 397


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== gitLoader relevant section =="
sed -n '980,1055p' quartz/plugins/loader/gitLoader.ts

echo
echo "== config-loader relevant section =="
sed -n '190,225p' quartz/plugins/loader/config-loader.ts

echo
echo "== package references =="
rg -n "npmPackages|options\\.verbose|Skipping npm package|npmPackages" quartz/plugins/loader/config-loader.ts quartz/plugins/loader/gitLoader.ts

Repository: Faustze/notes

Length of output: 6337


Resolve npmPackages manifests from the project root.

gitLoader.ts resolves npmPackages with createRequire(import.meta.url), which can use Quartz’s module search path instead of the project root where users install npm plugin packages. Align this with config-loader.ts’s manifest resolution and pass { paths: [process.cwd()] } so installed packages are discovered consistently.

Proposed fix
       const esmRequire = createRequire(import.meta.url)
-      const pkgJsonPath = esmRequire.resolve(`${npmPkg}/package.json`)
+      const pkgJsonPath = esmRequire.resolve(`${npmPkg}/package.json`, { paths: [process.cwd()] })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const npmPkg of options.npmPackages ?? []) {
let distIndex: string | undefined
try {
const esmRequire = createRequire(import.meta.url)
const pkgJsonPath = esmRequire.resolve(`${npmPkg}/package.json`)
distIndex = path.join(path.dirname(pkgJsonPath), "dist", "index.d.ts")
} catch {
if (options.verbose) {
console.log(styleText("yellow", `⚠`), `Skipping npm package ${npmPkg}: not found`)
}
continue
}
for (const npmPkg of options.npmPackages ?? []) {
let distIndex: string | undefined
try {
const esmRequire = createRequire(import.meta.url)
const pkgJsonPath = esmRequire.resolve(`${npmPkg}/package.json`, { paths: [process.cwd()] })
distIndex = path.join(path.dirname(pkgJsonPath), "dist", "index.d.ts")
} catch {
if (options.verbose) {
console.log(styleText("yellow", `⚠`), `Skipping npm package ${npmPkg}: not found`)
}
continue
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@quartz/plugins/loader/gitLoader.ts` around lines 1018 - 1029, Update the npm
package manifest resolution in the npmPackages loop to use createRequire with
the project root search path, passing { paths: [process.cwd()] } when resolving
`${npmPkg}/package.json`. Keep the existing dist/index.d.ts construction and
missing-package handling unchanged, matching the resolution approach used by
config-loader.ts.

@Faustze
Faustze merged commit be4aa8c into main Jul 23, 2026
7 checks passed
@Faustze
Faustze deleted the upstream-sync-v5 branch July 23, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants