From 5a6a2515ca9d940e22068f43b5073dd41cff22d6 Mon Sep 17 00:00:00 2001 From: saberzero1 Date: Sun, 8 Feb 2026 12:01:03 +0100 Subject: [PATCH] Implement Git-based plugin system with dogfooding for community plugins - Remove npm dependencies for @quartz-community/* plugins - Add gitLoader.ts for installing plugins from GitHub - Update quartz.layout.ts to import from .quartz/plugins/ - Add install-plugins.ts script for prebuild hook - Add .quartz/ to .gitignore --- .gitignore | 1 + content/index.md | 18 + content/test-page.md | 14 + package-lock.json | 716 +++++++++++++++++++++++ package.json | 7 +- quartz.config.ts | 6 +- quartz.layout.ts | 10 +- quartz/plugins/loader/gitLoader.ts | 251 ++++++++ quartz/plugins/loader/index.ts | 99 ++++ quartz/plugins/loader/install-plugins.ts | 30 + 10 files changed, 1144 insertions(+), 8 deletions(-) create mode 100644 content/index.md create mode 100644 content/test-page.md create mode 100644 quartz/plugins/loader/gitLoader.ts create mode 100644 quartz/plugins/loader/install-plugins.ts diff --git a/.gitignore b/.gitignore index 25d07db1c..168ad8325 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ tsconfig.tsbuildinfo private/ .replit replit.nix +.quartz/ diff --git a/content/index.md b/content/index.md new file mode 100644 index 000000000..daca95a18 --- /dev/null +++ b/content/index.md @@ -0,0 +1,18 @@ +--- +title: Welcome to Quartz +tags: + - documentation +--- + +This is the homepage of Quartz. + +## Getting Started + +Quartz is a static site generator for digital gardens. + +### Features + +- Fast search +- Graph view +- Explorer sidebar + diff --git a/content/test-page.md b/content/test-page.md new file mode 100644 index 000000000..c51120b52 --- /dev/null +++ b/content/test-page.md @@ -0,0 +1,14 @@ +--- +title: Test Page +tags: + - test + - example +--- + +This is a test page for search functionality. + +## Search Test Section + +You can search for terms like "test", "search", or "Quartz". + +More content here to make the page searchable. diff --git a/package-lock.json b/package-lock.json index facf76456..136ad8238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@napi-rs/simple-git": "0.1.22", "@quartz-community/explorer": "github:quartz-community/explorer", "@quartz-community/graph": "github:quartz-community/graph", + "@quartz-community/search": "github:quartz-community/search", "@tweenjs/tween.js": "^25.0.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", @@ -30,6 +31,7 @@ "hast-util-to-jsx-runtime": "^2.3.6", "hast-util-to-string": "^3.0.1", "is-absolute-url": "^5.0.0", + "isomorphic-git": "^1.36.3", "js-yaml": "^4.1.1", "lightningcss": "^1.31.1", "mdast-util-find-and-replace": "^3.0.2", @@ -827,6 +829,8 @@ }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", "cpu": [ "x64" ], @@ -993,6 +997,8 @@ }, "node_modules/@img/sharp-linuxmusl-x64": { "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", "cpu": [ "x64" ], @@ -1266,6 +1272,8 @@ }, "node_modules/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl": { "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@myriaddreamin/typst-ts-node-compiler-linux-x64-musl/-/typst-ts-node-compiler-linux-x64-musl-0.6.0.tgz", + "integrity": "sha512-b+kTb4vI0sFTkPtIAUE+UqjhZ4kTiAkh4F/2QKnFitAsURlLcRwTcMc9NJm6SXwW1OM0nPj1IGTfUOFpqLOIPQ==", "cpu": [ "x64" ], @@ -1510,6 +1518,8 @@ }, "node_modules/@napi-rs/simple-git-linux-x64-musl": { "version": "0.1.22", + "resolved": "https://registry.npmjs.org/@napi-rs/simple-git-linux-x64-musl/-/simple-git-linux-x64-musl-0.1.22.tgz", + "integrity": "sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==", "cpu": [ "x64" ], @@ -1813,6 +1823,8 @@ }, "node_modules/@parcel/watcher-linux-x64-musl": { "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.4.tgz", + "integrity": "sha512-YoRWCVgxv8akZrMhdyVi6/TyoeeMkQ0PGGOf2E4omODrvd1wxniXP+DBynKoHryStks7l+fDAMUBRzqNHrVOpg==", "cpu": [ "x64" ], @@ -1966,6 +1978,47 @@ } } }, + "node_modules/@quartz-community/search": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/quartz-community/search.git#e3b491b0a588c67ec5475cc7b1ccc35de7d8cf59", + "license": "MIT", + "dependencies": { + "@quartz-community/types": "github:quartz-community/types", + "mdast-util-find-and-replace": "^3.0.1", + "rehype-slug": "^6.0.0", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.3" + }, + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + }, + "peerDependencies": { + "@jackyzha0/quartz": "^4.5.2", + "preact": "^10.0.0" + }, + "peerDependenciesMeta": { + "@jackyzha0/quartz": { + "optional": true + }, + "preact": { + "optional": false + } + } + }, + "node_modules/@quartz-community/types": { + "version": "0.1.0", + "resolved": "git+ssh://git@github.com/quartz-community/types.git#a342579c845f6dfd74c2aed861b4662a69c5e328", + "license": "MIT", + "engines": { + "node": ">=22", + "npm": ">=10.9.2" + } + }, "node_modules/@shikijs/core": { "version": "1.26.2", "license": "MIT", @@ -2401,6 +2454,18 @@ "node": ">=10.0.0" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/agent-base": { "version": "7.1.0", "license": "MIT", @@ -2450,6 +2515,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==", + "license": "MIT" + }, "node_modules/async-mutex": { "version": "0.5.0", "license": "MIT", @@ -2457,6 +2528,21 @@ "tslib": "^2.4.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bail": { "version": "2.0.2", "license": "MIT", @@ -2530,6 +2616,53 @@ "node": ">= 0.8" } }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/camelize": { "version": "1.0.1", "license": "MIT", @@ -2594,6 +2727,12 @@ "version": "2.4.63", "license": "CPAL-1.0 OR AGPL-1.0" }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==", + "license": "Apache-2.0" + }, "node_modules/cli-spinner": { "version": "0.2.10", "license": "MIT", @@ -2649,6 +2788,18 @@ "node": ">= 0.6" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-fetch": { "version": "4.0.0", "license": "MIT", @@ -3053,6 +3204,38 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delaunator": { "version": "5.0.0", "license": "ISC", @@ -3085,6 +3268,26 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==", + "license": "MIT" + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/earcut": { "version": "3.0.2", "license": "ISC" @@ -3107,6 +3310,36 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/esbuild": { "version": "0.27.2", "hasInstallScript": true, @@ -3206,10 +3439,28 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "license": "MIT" }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/extend": { "version": "3.0.2", "license": "MIT" @@ -3345,6 +3596,21 @@ ], "license": "Apache-2.0" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/format": { "version": "0.2.2", "engines": { @@ -3390,6 +3656,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-tsconfig": { "version": "4.7.5", "dev": true, @@ -3440,6 +3743,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gray-matter": { "version": "4.0.3", "license": "MIT", @@ -3480,6 +3795,45 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "license": "MIT", @@ -3875,6 +4229,12 @@ "version": "5.1.4", "license": "MIT" }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/inline-style-parser": { "version": "0.2.4", "license": "MIT" @@ -3916,6 +4276,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.1", "license": "MIT", @@ -3996,10 +4368,71 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, "node_modules/ismobilejs": { "version": "1.1.1", "license": "MIT" }, + "node_modules/isomorphic-git": { + "version": "1.36.3", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.36.3.tgz", + "integrity": "sha512-bHF1nQTjL0IfSo13BHDO8oQ6SvYNQduTAdPJdSmrJ5JwZY2fsyjLujEXav5hqPCegSCAnc75ZsBUHqT/NqR7QA==", + "license": "MIT", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^4.0.0", + "sha.js": "^2.4.12", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/isomorphic-git/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/isomorphic-git/node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/js-binary-schema-parser": { "version": "2.0.3", "license": "MIT" @@ -4229,6 +4662,8 @@ }, "node_modules/lightningcss-linux-x64-musl": { "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -4316,6 +4751,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mathjax-full": { "version": "3.2.2", "license": "Apache-2.0", @@ -5238,6 +5682,18 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.1.1", "license": "BlueOak-1.0.0", @@ -5251,6 +5707,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/mj-context-menu": { "version": "0.6.1", "license": "Apache-2.0" @@ -5313,6 +5787,15 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-to-es": { "version": "1.0.0", "license": "MIT", @@ -5416,6 +5899,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pixi.js": { "version": "8.15.0", "license": "MIT", @@ -5437,6 +5929,15 @@ "url": "https://opencollective.com/pixijs" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss-value-parser": { "version": "4.2.0", "license": "MIT" @@ -5488,6 +5989,15 @@ "node": ">=4" } }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/property-information": { "version": "6.2.0", "license": "MIT", @@ -5521,6 +6031,46 @@ "node": ">= 0.6" } }, + "node_modules/readable-stream": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", + "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readable-stream/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/readdirp": { "version": "5.0.0", "license": "MIT", @@ -5955,6 +6505,26 @@ "tslib": "^2.1.0" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT" @@ -6410,6 +6980,43 @@ "node": "*" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/sharp": { "version": "0.34.5", "hasInstallScript": true, @@ -6467,6 +7074,51 @@ "@types/hast": "^3.0.4" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "license": "MIT" @@ -6534,6 +7186,15 @@ "version": "1.0.3", "license": "BSD-3-Clause" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-width": { "version": "7.2.0", "license": "MIT", @@ -6667,6 +7328,20 @@ "node": ">=12" } }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "license": "MIT", @@ -6730,6 +7405,20 @@ "fsevents": "~2.3.3" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/typescript": { "version": "5.9.3", "dev": true, @@ -6981,6 +7670,27 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wicked-good-xpath": { "version": "1.3.0", "license": "MIT" @@ -7004,6 +7714,12 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/ws": { "version": "8.19.0", "license": "MIT", diff --git a/package.json b/package.json index 26463758b..481ae1d27 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,9 @@ "check": "tsc --noEmit && npx prettier . --check", "format": "npx prettier . --write", "test": "tsx --test", - "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1" + "profile": "0x -D prof ./quartz/bootstrap-cli.mjs build --concurrency=1", + "install-plugins": "npx tsx ./quartz/plugins/loader/install-plugins.ts", + "prebuild": "npm run install-plugins" }, "engines": { "npm": ">=10.9.2", @@ -39,8 +41,6 @@ "@floating-ui/dom": "^1.7.4", "@myriaddreamin/rehype-typst": "^0.6.0", "@napi-rs/simple-git": "0.1.22", - "@quartz-community/explorer": "github:quartz-community/explorer", - "@quartz-community/graph": "github:quartz-community/graph", "@tweenjs/tween.js": "^25.0.0", "ansi-truncate": "^1.4.0", "async-mutex": "^0.5.0", @@ -56,6 +56,7 @@ "hast-util-to-jsx-runtime": "^2.3.6", "hast-util-to-string": "^3.0.1", "is-absolute-url": "^5.0.0", + "isomorphic-git": "^1.36.3", "js-yaml": "^4.1.1", "lightningcss": "^1.31.1", "mdast-util-find-and-replace": "^3.0.2", diff --git a/quartz.config.ts b/quartz.config.ts index fd9270478..420471f1b 100644 --- a/quartz.config.ts +++ b/quartz.config.ts @@ -92,7 +92,11 @@ const config: QuartzConfig = { Plugin.CustomOgImages(), ], }, - externalPlugins: ["@quartz-community/explorer", "@quartz-community/graph"], + externalPlugins: [ + "github:quartz-community/explorer", + "github:quartz-community/graph", + "github:quartz-community/search", + ], } export default config diff --git a/quartz.layout.ts b/quartz.layout.ts index d15bca85e..ee38d0c47 100644 --- a/quartz.layout.ts +++ b/quartz.layout.ts @@ -1,10 +1,12 @@ import { PageLayout, SharedLayout } from "./quartz/cfg" import * as Component from "./quartz/components" -import { Explorer } from "@quartz-community/explorer" -import { Graph } from "@quartz-community/graph" +import Explorer from "./.quartz/plugins/explorer/src/components/Explorer" +import Graph from "./.quartz/plugins/graph/src/components/Graph" +import Search from "./.quartz/plugins/search/src/components/Search" const explorerComponent = Explorer() const graphComponent = Graph() +const searchComponent = Search() // components shared across all pages export const sharedPageComponents: SharedLayout = { @@ -36,7 +38,7 @@ export const defaultContentPageLayout: PageLayout = { Component.Flex({ components: [ { - Component: Component.Search(), + Component: searchComponent, grow: true, }, { Component: Component.Darkmode() }, @@ -61,7 +63,7 @@ export const defaultListPageLayout: PageLayout = { Component.Flex({ components: [ { - Component: Component.Search(), + Component: searchComponent, grow: true, }, { Component: Component.Darkmode() }, diff --git a/quartz/plugins/loader/gitLoader.ts b/quartz/plugins/loader/gitLoader.ts new file mode 100644 index 000000000..973bd1c64 --- /dev/null +++ b/quartz/plugins/loader/gitLoader.ts @@ -0,0 +1,251 @@ +import fs from "fs" +import path from "path" +import git from "isomorphic-git" +import http from "isomorphic-git/http/node" +import { styleText } from "util" + +export interface GitPluginSpec { + /** Plugin name (used for directory) */ + name: string + /** Git repository URL (https://github.com/user/repo.git or just github:user/repo) */ + repo: string + /** Git ref (branch, tag, or commit hash). Defaults to 'main' */ + ref?: string + /** Optional subdirectory within the repo if plugin is not at root */ + subdir?: string +} + +export type PluginInstallSource = string | GitPluginSpec + +const PLUGINS_CACHE_DIR = path.join(process.cwd(), ".quartz", "plugins") + +/** + * Parse a plugin source string into a GitPluginSpec + * Supports: + * - "github:user/repo" -> https://github.com/user/repo.git + * - "github:user/repo#ref" -> https://github.com/user/repo.git with specific ref + * - "git+https://..." -> direct git URL + * - "https://github.com/..." -> direct https URL + */ +export function parsePluginSource(source: string): GitPluginSpec { + // Handle github shorthand: github:user/repo or github:user/repo#ref + if (source.startsWith("github:")) { + const withoutPrefix = source.replace("github:", "") + const [repoPath, ref] = withoutPrefix.split("#") + const [owner, repo] = repoPath.split("/") + + if (!owner || !repo) { + throw new Error(`Invalid GitHub source: ${source}. Expected format: github:user/repo`) + } + + return { + name: repo, + repo: `https://github.com/${owner}/${repo}.git`, + ref: ref || "main", + } + } + + // Handle git+https:// protocol + if (source.startsWith("git+")) { + const url = source.replace("git+", "") + const name = extractRepoName(url) + return { name, repo: url, ref: "main" } + } + + // Handle direct HTTPS URL (GitHub, GitLab, etc.) + if (source.startsWith("https://")) { + const name = extractRepoName(source) + return { name, repo: source, ref: "main" } + } + + // Assume it's a plain repo name and try github + const parts = source.split("/") + if (parts.length === 2) { + return { + name: parts[1], + repo: `https://github.com/${source}.git`, + ref: "main", + } + } + + throw new Error(`Cannot parse plugin source: ${source}`) +} + +function extractRepoName(url: string): string { + // Extract repo name from URL like https://github.com/user/repo.git + const match = url.match(/\/([^\/]+?)(?:\.git)?$/) + return match ? match[1] : "unknown" +} + +/** + * Install a plugin from a Git repository + */ +export async function installPlugin( + spec: GitPluginSpec, + options: { verbose?: boolean; force?: boolean } = {}, +): Promise { + const pluginDir = path.join(PLUGINS_CACHE_DIR, spec.name) + + // Check if already installed + if (!options.force && fs.existsSync(pluginDir)) { + // Check if it's a git repo by trying to resolve HEAD + try { + await git.resolveRef({ fs, dir: pluginDir, ref: "HEAD" }) + if (options.verbose) { + console.log(styleText("cyan", `→`), `Plugin ${spec.name} already installed`) + } + return pluginDir + } catch { + // If git operations fail, re-clone + } + } + + // Clean up if force reinstall + if (options.force && fs.existsSync(pluginDir)) { + fs.rmSync(pluginDir, { recursive: true }) + } + + if (options.verbose) { + console.log(styleText("cyan", `→`), `Cloning ${spec.name} from ${spec.repo}#${spec.ref}...`) + } + + // Clone the repository + await git.clone({ + fs, + http, + dir: pluginDir, + url: spec.repo, + ref: spec.ref, + singleBranch: true, + depth: 1, + noCheckout: false, + }) + + if (options.verbose) { + console.log(styleText("green", `✓`), `Installed ${spec.name}`) + } + + return pluginDir +} + +/** + * Install multiple plugins from Git repositories + */ +export async function installPlugins( + sources: PluginInstallSource[], + options: { verbose?: boolean; force?: boolean } = {}, +): Promise> { + const installed = new Map() + + for (const source of sources) { + try { + const spec = typeof source === "string" ? parsePluginSource(source) : source + const pluginDir = await installPlugin(spec, options) + installed.set(spec.name, pluginDir) + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + console.error(styleText("red", `✗`), `Failed to install plugin: ${message}`) + } + } + + return installed +} + +/** + * Get the installation directory for a plugin + */ +export function getPluginDir(name: string): string { + return path.join(PLUGINS_CACHE_DIR, name) +} + +/** + * Check if a plugin is installed + */ +export function isPluginInstalled(name: string): boolean { + return fs.existsSync(getPluginDir(name)) +} + +/** + * Get the entry point for a plugin + */ +export function getPluginEntryPoint(name: string, subdir?: string): string { + const pluginDir = getPluginDir(name) + const searchDir = subdir ? path.join(pluginDir, subdir) : pluginDir + + // Try common entry points + const candidates = [ + path.join(searchDir, "src", "index.ts"), + path.join(searchDir, "src", "index.js"), + path.join(searchDir, "index.ts"), + path.join(searchDir, "index.js"), + path.join(searchDir, "dist", "index.js"), + ] + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate + } + } + + // If no entry found, return the search dir and let Node handle it + return searchDir +} + +/** + * Update all installed plugins + */ +export async function updatePlugins(options: { verbose?: boolean } = {}): Promise { + if (!fs.existsSync(PLUGINS_CACHE_DIR)) { + console.log("No plugins installed") + return + } + + const plugins = fs.readdirSync(PLUGINS_CACHE_DIR) + + for (const pluginName of plugins) { + const pluginDir = path.join(PLUGINS_CACHE_DIR, pluginName) + + try { + // Check if it's a git repo + await git.resolveRef({ fs, dir: pluginDir, ref: "HEAD" }) + + if (options.verbose) { + console.log(styleText("cyan", `→`), `Updating ${pluginName}...`) + } + + // Fetch latest + await git.fetch({ + fs, + http, + dir: pluginDir, + singleBranch: true, + }) + + // Checkout to latest fetched commit + await git.checkout({ + fs, + dir: pluginDir, + ref: "FETCH_HEAD", + force: true, + }) + + if (options.verbose) { + console.log(styleText("green", `✓`), `Updated ${pluginName}`) + } + } catch (error) { + if (options.verbose) { + console.error(styleText("yellow", `⚠`), `Skipping ${pluginName}: Not a git repo`) + } + } + } +} + +/** + * Clean all installed plugins + */ +export function cleanPlugins(): void { + if (fs.existsSync(PLUGINS_CACHE_DIR)) { + fs.rmSync(PLUGINS_CACHE_DIR, { recursive: true }) + console.log(styleText("green", `✓`), "Cleaned all plugins") + } +} diff --git a/quartz/plugins/loader/index.ts b/quartz/plugins/loader/index.ts index 1993cf436..b9f21e4fe 100644 --- a/quartz/plugins/loader/index.ts +++ b/quartz/plugins/loader/index.ts @@ -8,6 +8,7 @@ import { PluginSpecifier, } from "./types" import { QuartzTransformerPlugin, QuartzFilterPlugin, QuartzEmitterPlugin } from "../types" +import { parsePluginSource, installPlugin, getPluginEntryPoint } from "./gitLoader" const MINIMUM_QUARTZ_VERSION = "4.5.0" @@ -93,17 +94,36 @@ function extractPluginFactory( return null } +function isGitSource(source: string): boolean { + // Check if it's a Git-based source + return ( + source.startsWith("github:") || + source.startsWith("git+") || + source.startsWith("https://github.com/") || + source.startsWith("https://gitlab.com/") || + source.startsWith("https://bitbucket.org/") + ) +} + async function resolveSinglePlugin( specifier: PluginSpecifier, options: PluginResolutionOptions, ): Promise<{ plugin: LoadedPlugin | null; error: PluginResolutionError | null }> { let packageName: string let manifest: Partial = {} + let pluginSource = "npm" if (typeof specifier === "string") { packageName = specifier + // Check if it's a Git-based source + if (isGitSource(specifier)) { + pluginSource = "git" + } } else if ("name" in specifier) { packageName = specifier.name + if (isGitSource(specifier.name)) { + pluginSource = "git" + } } else if ("plugin" in specifier) { const type = specifier.manifest?.category ?? "transformer" return { @@ -133,6 +153,85 @@ async function resolveSinglePlugin( } } + if (pluginSource === "git") { + try { + const gitSpec = parsePluginSource(packageName) + await installPlugin(gitSpec, { verbose: options.verbose }) + const entryPoint = getPluginEntryPoint(gitSpec.name, gitSpec.subdir) + + // Import the plugin + const module = await import(entryPoint) + const importedManifest: PluginManifest | null = module.manifest ?? null + + manifest = importedManifest ?? {} + + const detectedType = manifest.category ?? detectPluginType(module) + + if (!detectedType) { + return { + plugin: null, + error: { + plugin: packageName, + message: "Could not detect plugin type from Git source", + type: "invalid-manifest", + }, + } + } + + const factory = extractPluginFactory(module, detectedType) + + if (!factory) { + return { + plugin: null, + error: { + plugin: packageName, + message: "Could not find plugin factory in Git source", + type: "invalid-manifest", + }, + } + } + + const fullManifest: PluginManifest = { + name: manifest.name ?? gitSpec.name, + displayName: manifest.displayName ?? gitSpec.name, + 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: `${gitSpec.repo}#${gitSpec.ref}`, + } + + if (options.verbose) { + console.log( + styleText("green", `✓`) + + ` Loaded ${detectedType} plugin: ${styleText("cyan", fullManifest.displayName)}@${fullManifest.version} ${styleText("gray", `(from ${gitSpec.repo})`)}`, + ) + } + + return { plugin: loadedPlugin, error: null } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + return { + plugin: null, + error: { + plugin: packageName, + message: `Failed to load Git plugin: ${errorMessage}`, + type: "import-error", + }, + } + } + } + try { const { module: importedModule, manifest: importedManifest } = await tryImportPlugin(packageName) diff --git a/quartz/plugins/loader/install-plugins.ts b/quartz/plugins/loader/install-plugins.ts new file mode 100644 index 000000000..8bc20e5ee --- /dev/null +++ b/quartz/plugins/loader/install-plugins.ts @@ -0,0 +1,30 @@ +#!/usr/bin/env node +import { installPlugins, parsePluginSource } from "./gitLoader.js" +import config from "../../../quartz.config.js" + +async function main() { + const quartzConfig: any = config + const externalPlugins = quartzConfig.externalPlugins || [] + + if (externalPlugins.length === 0) { + console.log("No external plugins to install.") + return + } + + console.log(`Installing ${externalPlugins.length} plugin(s) from Git...`) + + const specs = externalPlugins.map((source: string) => parsePluginSource(source)) + const installed = await installPlugins(specs, { verbose: true }) + + if (installed.size === externalPlugins.length) { + console.log("✓ All plugins installed successfully") + } else { + console.error(`✗ Only ${installed.size}/${externalPlugins.length} plugins installed`) + process.exit(1) + } +} + +main().catch((err) => { + console.error("Failed to install plugins:", err) + process.exit(1) +})