feat: support npm scoped package specifiers for plugins
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
This commit is contained in:
@@ -47,6 +47,7 @@
|
|||||||
"esbuild-sass-plugin": "^3.6.0",
|
"esbuild-sass-plugin": "^3.6.0",
|
||||||
"github-slugger": "^2.0.0",
|
"github-slugger": "^2.0.0",
|
||||||
"globby": "^16.1.0",
|
"globby": "^16.1.0",
|
||||||
|
"hast-util-from-html": "^2.0.0",
|
||||||
"hast-util-to-jsx-runtime": "^2.3.6",
|
"hast-util-to-jsx-runtime": "^2.3.6",
|
||||||
"isomorphic-git": "^1.36.3",
|
"isomorphic-git": "^1.36.3",
|
||||||
"lightningcss": "^1.31.1",
|
"lightningcss": "^1.31.1",
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
configuration:
|
||||||
|
pageTitle: Quartz 5
|
||||||
|
pageTitleSuffix: ""
|
||||||
|
enableSPA: true
|
||||||
|
enablePopovers: true
|
||||||
|
analytics:
|
||||||
|
provider: plausible
|
||||||
|
locale: en-US
|
||||||
|
baseUrl: quartz.jzhao.xyz
|
||||||
|
ignorePatterns:
|
||||||
|
- private
|
||||||
|
- templates
|
||||||
|
- .obsidian
|
||||||
|
theme:
|
||||||
|
fontOrigin: googleFonts
|
||||||
|
cdnCaching: true
|
||||||
|
typography:
|
||||||
|
header: Schibsted Grotesk
|
||||||
|
body: Source Sans Pro
|
||||||
|
code: IBM Plex Mono
|
||||||
|
colors:
|
||||||
|
lightMode:
|
||||||
|
light: "#faf8f8"
|
||||||
|
lightgray: "#e5e5e5"
|
||||||
|
gray: "#b8b8b8"
|
||||||
|
darkgray: "#4e4e4e"
|
||||||
|
dark: "#2b2b2b"
|
||||||
|
secondary: "#284b63"
|
||||||
|
tertiary: "#84a59d"
|
||||||
|
highlight: rgba(143, 159, 169, 0.15)
|
||||||
|
textHighlight: "#fff23688"
|
||||||
|
darkMode:
|
||||||
|
light: "#161618"
|
||||||
|
lightgray: "#393639"
|
||||||
|
gray: "#646464"
|
||||||
|
darkgray: "#d4d4d4"
|
||||||
|
dark: "#ebebec"
|
||||||
|
secondary: "#7b97aa"
|
||||||
|
tertiary: "#84a59d"
|
||||||
|
highlight: rgba(143, 159, 169, 0.15)
|
||||||
|
textHighlight: "#b3aa0288"
|
||||||
|
plugins:
|
||||||
|
- source: ../created-modified-date
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
defaultDateType: modified
|
||||||
|
priority:
|
||||||
|
- frontmatter
|
||||||
|
- git
|
||||||
|
- filesystem
|
||||||
|
order: 10
|
||||||
|
- source: ../syntax-highlighting
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
theme:
|
||||||
|
light: github-light
|
||||||
|
dark: github-dark
|
||||||
|
keepBackground: false
|
||||||
|
order: 20
|
||||||
|
- source: ../obsidian-flavored-markdown
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
enableInHtmlEmbed: false
|
||||||
|
enableCheckbox: true
|
||||||
|
order: 30
|
||||||
|
- source: ../github-flavored-markdown
|
||||||
|
enabled: true
|
||||||
|
order: 40
|
||||||
|
- source: ../table-of-contents
|
||||||
|
enabled: true
|
||||||
|
order: 50
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 30
|
||||||
|
- source: ../crawl-links
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
markdownLinkResolution: shortest
|
||||||
|
order: 60
|
||||||
|
- source: ../description
|
||||||
|
enabled: true
|
||||||
|
order: 70
|
||||||
|
- source: ../latex
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
renderEngine: katex
|
||||||
|
order: 80
|
||||||
|
- source: ../citations
|
||||||
|
enabled: false
|
||||||
|
order: 85
|
||||||
|
- source: ../hard-line-breaks
|
||||||
|
enabled: false
|
||||||
|
order: 90
|
||||||
|
- source: ../ox-hugo
|
||||||
|
enabled: false
|
||||||
|
order: 91
|
||||||
|
- source: ../roam
|
||||||
|
enabled: false
|
||||||
|
order: 92
|
||||||
|
- source: ../fonts
|
||||||
|
enabled: true
|
||||||
|
- source: ../remove-draft
|
||||||
|
enabled: true
|
||||||
|
- source: ../explicit-publish
|
||||||
|
enabled: false
|
||||||
|
- source: ../alias-redirects
|
||||||
|
enabled: true
|
||||||
|
- source: ../content-index
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
enableSiteMap: true
|
||||||
|
enableRSS: true
|
||||||
|
- source: ../favicon
|
||||||
|
enabled: true
|
||||||
|
- source: ../og-image
|
||||||
|
enabled: true
|
||||||
|
- source: ../cname
|
||||||
|
enabled: true
|
||||||
|
- source: ../canvas-page
|
||||||
|
enabled: true
|
||||||
|
- source: ../content-page
|
||||||
|
enabled: true
|
||||||
|
- source: ../folder-page
|
||||||
|
enabled: true
|
||||||
|
- source: ../tag-page
|
||||||
|
enabled: true
|
||||||
|
- source: ../explorer
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 50
|
||||||
|
- source: ../graph
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 10
|
||||||
|
- source: ../search
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 20
|
||||||
|
group: toolbar
|
||||||
|
groupOptions:
|
||||||
|
grow: true
|
||||||
|
- source: ../backlinks
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: right
|
||||||
|
priority: 50
|
||||||
|
- source: ../article-title
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 10
|
||||||
|
- source: ../content-meta
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 20
|
||||||
|
- source: ../tag-list
|
||||||
|
enabled: false
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 30
|
||||||
|
- source: ../page-title
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 10
|
||||||
|
- source: ../darkmode
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 30
|
||||||
|
group: toolbar
|
||||||
|
- source: ../reader-mode
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 35
|
||||||
|
group: toolbar
|
||||||
|
- source: ../breadcrumbs
|
||||||
|
enabled: true
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 5
|
||||||
|
condition: not-index
|
||||||
|
- source: ../comments
|
||||||
|
enabled: false
|
||||||
|
options:
|
||||||
|
provider: giscus
|
||||||
|
options: {}
|
||||||
|
layout:
|
||||||
|
position: afterBody
|
||||||
|
priority: 10
|
||||||
|
- source: ../footer
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
links:
|
||||||
|
GitHub: https://github.com/jackyzha0/quartz
|
||||||
|
Discord Community: https://discord.gg/cRFFHYye7t
|
||||||
|
- source: ../recent-notes
|
||||||
|
enabled: false
|
||||||
|
- source: ../spacer
|
||||||
|
enabled: true
|
||||||
|
options: {}
|
||||||
|
order: 25
|
||||||
|
layout:
|
||||||
|
position: left
|
||||||
|
priority: 25
|
||||||
|
display: mobile-only
|
||||||
|
- source: ../bases-page
|
||||||
|
enabled: true
|
||||||
|
options: {}
|
||||||
|
order: 50
|
||||||
|
- source: ../note-properties
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
includeAll: false
|
||||||
|
includedProperties:
|
||||||
|
- description
|
||||||
|
- tags
|
||||||
|
- aliases
|
||||||
|
excludedProperties: []
|
||||||
|
hidePropertiesView: false
|
||||||
|
delimiters: ---
|
||||||
|
language: yaml
|
||||||
|
order: 5
|
||||||
|
layout:
|
||||||
|
position: beforeBody
|
||||||
|
priority: 15
|
||||||
|
display: all
|
||||||
|
- source: ../unlisted-pages
|
||||||
|
enabled: true
|
||||||
|
options: {}
|
||||||
|
order: 45
|
||||||
|
- source: ../encrypted-pages
|
||||||
|
enabled: true
|
||||||
|
options:
|
||||||
|
iterations: 600000
|
||||||
|
passwordField: password
|
||||||
|
unlistWhenEncrypted: false
|
||||||
|
outputPath: static/encryptedContentIndex.json
|
||||||
|
order: 900
|
||||||
|
layout:
|
||||||
|
position: body
|
||||||
|
priority: 100
|
||||||
|
display: all
|
||||||
|
- source: ../stacked-pages
|
||||||
|
enabled: false
|
||||||
|
layout:
|
||||||
|
position: afterBody
|
||||||
|
priority: 50
|
||||||
|
display: all
|
||||||
|
layout:
|
||||||
|
groups:
|
||||||
|
toolbar:
|
||||||
|
priority: 35
|
||||||
|
direction: row
|
||||||
|
gap: 0.5rem
|
||||||
|
byPageType:
|
||||||
|
"404":
|
||||||
|
positions:
|
||||||
|
beforeBody: []
|
||||||
|
left: []
|
||||||
|
right: []
|
||||||
|
content: {}
|
||||||
|
folder:
|
||||||
|
exclude:
|
||||||
|
- reader-mode
|
||||||
|
positions:
|
||||||
|
right: []
|
||||||
|
tag:
|
||||||
|
exclude:
|
||||||
|
- reader-mode
|
||||||
|
positions:
|
||||||
|
right: []
|
||||||
|
canvas: {}
|
||||||
|
bases: {}
|
||||||
@@ -190,6 +190,11 @@ export function parseGitSource(source) {
|
|||||||
typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git")
|
typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git")
|
||||||
return { name, url: parsed, ref, subdir }
|
return { name, url: parsed, ref, subdir }
|
||||||
}
|
}
|
||||||
|
// Handle npm scoped packages
|
||||||
|
if (typeof url === "string" && url.startsWith("@") && url.includes("/") && !url.includes(":")) {
|
||||||
|
const name = typeof source === "object" && source.name ? source.name : url
|
||||||
|
return { name, url: "", npmPackage: true, subdir }
|
||||||
|
}
|
||||||
throw new Error(`Cannot parse plugin source: ${formatSource(source)}`)
|
throw new Error(`Cannot parse plugin source: ${formatSource(source)}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs"
|
import fs from "fs"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import os from "os"
|
import os from "os"
|
||||||
import { exec as execCb } from "child_process"
|
import { exec as execCb, execSync } from "child_process"
|
||||||
import { styleText, promisify } from "util"
|
import { styleText, promisify } from "util"
|
||||||
import {
|
import {
|
||||||
readPluginsJson,
|
readPluginsJson,
|
||||||
@@ -1190,6 +1190,15 @@ export async function handlePluginAdd(
|
|||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
try {
|
try {
|
||||||
const parsed = parseGitSource(source)
|
const parsed = parseGitSource(source)
|
||||||
|
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
|
||||||
|
}
|
||||||
const name = nameOverride ?? parsed.name
|
const name = nameOverride ?? parsed.name
|
||||||
const url = parsed.url
|
const url = parsed.url
|
||||||
const ref = parsed.ref
|
const ref = parsed.ref
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fs from "fs"
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import YAML from "yaml"
|
import YAML from "yaml"
|
||||||
import { styleText } from "util"
|
import { styleText } from "util"
|
||||||
|
import { createRequire } from "node:module"
|
||||||
import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg"
|
import { QuartzConfig, GlobalConfiguration, FullPageLayout } from "../../cfg"
|
||||||
import { QuartzComponent, QuartzComponentConstructor } from "../../components/types"
|
import { QuartzComponent, QuartzComponentConstructor } from "../../components/types"
|
||||||
import { PluginTypes } from "../types"
|
import { PluginTypes } from "../types"
|
||||||
@@ -200,8 +201,14 @@ async function resolvePluginManifest(source: PluginSource): Promise<PluginManife
|
|||||||
async function readManifestFromPackageJson(source: PluginSource): Promise<PluginManifest | null> {
|
async function readManifestFromPackageJson(source: PluginSource): Promise<PluginManifest | null> {
|
||||||
try {
|
try {
|
||||||
const gitSpec = parsePluginSource(source)
|
const gitSpec = parsePluginSource(source)
|
||||||
const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name)
|
const require = createRequire(import.meta.url)
|
||||||
const pkgPath = path.join(pluginDir, "package.json")
|
let pkgPath: string
|
||||||
|
if (gitSpec.npmPackage) {
|
||||||
|
pkgPath = require.resolve(`${gitSpec.name}/package.json`, { paths: [process.cwd()] })
|
||||||
|
} else {
|
||||||
|
const pluginDir = path.join(process.cwd(), ".quartz", "plugins", gitSpec.name)
|
||||||
|
pkgPath = path.join(pluginDir, "package.json")
|
||||||
|
}
|
||||||
if (!fs.existsSync(pkgPath)) return null
|
if (!fs.existsSync(pkgPath)) return null
|
||||||
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||||
@@ -259,6 +266,9 @@ export async function loadQuartzConfig(
|
|||||||
for (const entry of enabledEntries) {
|
for (const entry of enabledEntries) {
|
||||||
try {
|
try {
|
||||||
const gitSpec = parsePluginSource(entry.source)
|
const gitSpec = parsePluginSource(entry.source)
|
||||||
|
if (gitSpec.npmPackage) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
const result = await installPlugin(gitSpec, { verbose: false })
|
const result = await installPlugin(gitSpec, { verbose: false })
|
||||||
if (result.nativeDeps.size > 0) {
|
if (result.nativeDeps.size > 0) {
|
||||||
allNativeDeps.set(gitSpec.name, result.nativeDeps)
|
allNativeDeps.set(gitSpec.name, result.nativeDeps)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ export interface GitPluginSpec {
|
|||||||
subdir?: string
|
subdir?: string
|
||||||
/** Whether this is a local path source */
|
/** Whether this is a local path source */
|
||||||
local?: boolean
|
local?: boolean
|
||||||
|
/** Whether this is an npm package (installed in node_modules) */
|
||||||
|
npmPackage?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PluginInstallSource = string | GitPluginSpec
|
export type PluginInstallSource = string | GitPluginSpec
|
||||||
@@ -87,6 +89,7 @@ export function parsePluginSource(source: PluginSource): GitPluginSpec {
|
|||||||
ref: ref || expanded.ref || undefined,
|
ref: ref || expanded.ref || undefined,
|
||||||
subdir,
|
subdir,
|
||||||
local: expanded.local,
|
local: expanded.local,
|
||||||
|
npmPackage: expanded.npmPackage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +132,11 @@ export function parsePluginSource(source: PluginSource): GitPluginSpec {
|
|||||||
return { name, repo: url, ref: ref || undefined }
|
return { name, repo: url, ref: ref || undefined }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle npm scoped packages (e.g. @quartz-community/syntax-highlighting)
|
||||||
|
if (typeof source === "string" && source.startsWith("@") && source.includes("/") && !source.includes(":")) {
|
||||||
|
return { name: source, repo: "", npmPackage: true }
|
||||||
|
}
|
||||||
|
|
||||||
// Assume it's a plain repo name and try github
|
// Assume it's a plain repo name and try github
|
||||||
const parts = source.split("/")
|
const parts = source.split("/")
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
|
|||||||
@@ -1,19 +1,68 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
import fs from "fs"
|
||||||
|
import path from "path"
|
||||||
|
import YAML from "yaml"
|
||||||
import { installPlugins, parsePluginSource } from "./gitLoader.js"
|
import { installPlugins, parsePluginSource } from "./gitLoader.js"
|
||||||
import config from "../../../quartz.js"
|
import type { PluginSource, QuartzPluginsJson } from "./types.js"
|
||||||
|
|
||||||
|
function resolveConfigPath(): string {
|
||||||
|
const configYamlPath = path.join(process.cwd(), "quartz.config.yaml")
|
||||||
|
const defaultConfigYamlPath = path.join(process.cwd(), "quartz.config.default.yaml")
|
||||||
|
const legacyPluginsJsonPath = path.join(process.cwd(), "quartz.plugins.json")
|
||||||
|
const legacyDefaultPluginsJsonPath = path.join(process.cwd(), "quartz.plugins.default.json")
|
||||||
|
|
||||||
|
if (fs.existsSync(configYamlPath)) return configYamlPath
|
||||||
|
if (fs.existsSync(legacyPluginsJsonPath)) return legacyPluginsJsonPath
|
||||||
|
if (fs.existsSync(defaultConfigYamlPath)) return defaultConfigYamlPath
|
||||||
|
if (fs.existsSync(legacyDefaultPluginsJsonPath)) return legacyDefaultPluginsJsonPath
|
||||||
|
return configYamlPath
|
||||||
|
}
|
||||||
|
|
||||||
|
function readPluginsJson(): QuartzPluginsJson | null {
|
||||||
|
const configPath = resolveConfigPath()
|
||||||
|
if (!fs.existsSync(configPath)) return null
|
||||||
|
const raw = fs.readFileSync(configPath, "utf-8")
|
||||||
|
if (configPath.endsWith(".yaml") || configPath.endsWith(".yml")) {
|
||||||
|
return YAML.parse(raw)
|
||||||
|
}
|
||||||
|
return JSON.parse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getExternalPluginSources(): Promise<PluginSource[]> {
|
||||||
|
try {
|
||||||
|
const module = await import("../../../quartz.js")
|
||||||
|
const config = module.default ?? module
|
||||||
|
const externalPlugins = config.externalPlugins
|
||||||
|
if (Array.isArray(externalPlugins) && externalPlugins.length > 0) {
|
||||||
|
return externalPlugins as PluginSource[]
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// fall back to config yaml parsing
|
||||||
|
}
|
||||||
|
|
||||||
|
const pluginsJson = readPluginsJson()
|
||||||
|
const entries = pluginsJson?.plugins ?? []
|
||||||
|
return entries.filter((entry) => entry.enabled !== false).map((entry) => entry.source)
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const quartzConfig: any = config
|
const externalPlugins = await getExternalPluginSources()
|
||||||
const externalPlugins = quartzConfig.externalPlugins || []
|
|
||||||
|
|
||||||
if (externalPlugins.length === 0) {
|
if (externalPlugins.length === 0) {
|
||||||
console.log("No external plugins to install.")
|
console.log("No external plugins to install.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Installing ${externalPlugins.length} plugin(s) from Git...`)
|
const specs = externalPlugins
|
||||||
|
.map((source: PluginSource) => parsePluginSource(source))
|
||||||
|
.filter((spec) => !spec.npmPackage)
|
||||||
|
|
||||||
const specs = externalPlugins.map((source: string) => parsePluginSource(source))
|
if (specs.length === 0) {
|
||||||
|
console.log("No external Git plugins to install.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Installing ${specs.length} plugin(s) from Git...`)
|
||||||
const installed = await installPlugins(specs, { verbose: true })
|
const installed = await installPlugins(specs, { verbose: true })
|
||||||
|
|
||||||
if (installed.size === externalPlugins.length) {
|
if (installed.size === externalPlugins.length) {
|
||||||
|
|||||||
Reference in New Issue
Block a user