/
superded
/
void
Обзор
Документация
Войти
/
superded
/
void
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
1
CI/CD
Аналитика
Безопасность
v5
quartz/cli/plugin-data.js
367 строк
12 KB
superded42
Initial commit
04 июл 2026, 05:05
Не верифицирован
04 июл 2026, 05:05
af1cbe5
Код
Авторство
О чём код?
import fs from "fs" import path from "path" import { execSync } from "child_process" import YAML from "yaml" const LOCKFILE_PATH = path.join(process.cwd(), "quartz.lock.json") const PLUGINS_DIR = path.join(process.cwd(), ".quartz", "plugins") const CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.yaml") const DEFAULT_CONFIG_YAML_PATH = path.join(process.cwd(), "quartz.config.default.yaml") const TEMPLATES_DIR = path.join(process.cwd(), "quartz", "cli", "templates") const LEGACY_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.json") const LEGACY_DEFAULT_PLUGINS_JSON_PATH = path.join(process.cwd(), "quartz.plugins.default.json") function resolveConfigPath() { if (fs.existsSync(CONFIG_YAML_PATH)) return CONFIG_YAML_PATH if (fs.existsSync(LEGACY_PLUGINS_JSON_PATH)) return LEGACY_PLUGINS_JSON_PATH if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH return CONFIG_YAML_PATH } function resolveDefaultConfigPath() { if (fs.existsSync(DEFAULT_CONFIG_YAML_PATH)) return DEFAULT_CONFIG_YAML_PATH if (fs.existsSync(LEGACY_DEFAULT_PLUGINS_JSON_PATH)) return LEGACY_DEFAULT_PLUGINS_JSON_PATH return DEFAULT_CONFIG_YAML_PATH } function readFileAsData(filePath) { if (!fs.existsSync(filePath)) return null try { const raw = fs.readFileSync(filePath, "utf-8") if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) { return YAML.parse(raw) } return JSON.parse(raw) } catch { return null } } function writeDataToFile(filePath, data) { if (filePath.endsWith(".yaml") || filePath.endsWith(".yml")) { const header = "# yaml-language-server: $schema=./quartz/plugins/quartz-plugins.schema.json\n" fs.writeFileSync(filePath, header + YAML.stringify(data, { lineWidth: 120 })) } else { fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n") } } export function readPluginsJson() { const configPath = resolveConfigPath() return readFileAsData(configPath) } export function writePluginsJson(data) { const { $schema, ...rest } = data writeDataToFile(CONFIG_YAML_PATH, rest) } function readDefaultPluginsJson() { const defaultPath = resolveDefaultConfigPath() return readFileAsData(defaultPath) } export function readLockfile() { if (!fs.existsSync(LOCKFILE_PATH)) return null try { return JSON.parse(fs.readFileSync(LOCKFILE_PATH, "utf-8")) } catch { return null } } export function writeLockfile(lockfile) { if (lockfile.plugins) { const sorted = {} for (const key of Object.keys(lockfile.plugins).sort()) { sorted[key] = lockfile.plugins[key] } lockfile = { ...lockfile, plugins: sorted } } fs.writeFileSync(LOCKFILE_PATH, JSON.stringify(lockfile, null, 2) + "\n") } /** * Normalizes a source value to a URL string. * Source can be a plain string (e.g. "github:owner/repo") or an object * with { name?, repo, subdir? } for installing from a subdirectory of a repo. */ export function getSourceUrl(source) { if (typeof source === "string") return source if (typeof source === "object" && source !== null && typeof source.repo === "string") { return source.repo } throw new Error(`Invalid plugin source: ${JSON.stringify(source)}`) } /** * Returns the subdir from an object source, or undefined for string sources. */ function getSourceSubdir(source) { if (typeof source === "object" && source !== null && typeof source.subdir === "string") { return source.subdir } return undefined } /** * Returns a display-friendly string for a source value. */ export function formatSource(source) { if (typeof source === "string") return source if (typeof source === "object" && source !== null) { const parts = [source.repo] if (source.subdir) parts.push(`(subdir: ${source.subdir})`) return parts.join(" ") } return String(source) } export function isLocalSource(source) { const url = getSourceUrl(source) if (url.startsWith("./") || url.startsWith("../") || url.startsWith("/")) { return true } // Windows absolute paths (e.g. C:\ or D:/) if (/^[A-Za-z]:[\\/]/.test(url)) { return true } return false } export function extractPluginName(source) { if (typeof source === "object" && source !== null && typeof source.name === "string") { return source.name } const url = getSourceUrl(source) if (isLocalSource(url)) { return path.basename(url.replace(/[\/]+$/, "")) } if (url.startsWith("github:")) { const withoutPrefix = url.replace("github:", "") const [repoPath] = withoutPrefix.split("#") const parts = repoPath.split("/") return parts[parts.length - 1] } if (url.startsWith("git+") || url.startsWith("https://")) { const cleaned = url.replace("git+", "") const match = cleaned.match(/\/([^/]+?)(?:\.git)?(?:#|$)/) return match?.[1] ?? url } return url } export function readManifestFromPackageJson(pluginDir) { const pkgPath = path.join(pluginDir, "package.json") if (!fs.existsSync(pkgPath)) return null try { const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) return pkg.quartz ?? null } catch { return null } } export function parseGitSource(source) { const url = getSourceUrl(source) const subdir = getSourceSubdir(source) if (isLocalSource(url)) { const resolved = path.resolve(url) const name = typeof source === "object" && source.name ? source.name : path.basename(resolved) return { name, url: resolved, ref: undefined, local: true, subdir } } if (url.startsWith("github:")) { const [repoPath, ref] = url.replace("github:", "").split("#") const [owner, repo] = repoPath.split("/") const name = typeof source === "object" && source.name ? source.name : repo return { name, url: `https://github.com/${owner}/${repo}.git`, ref, subdir } } if (url.startsWith("git+")) { const raw = url.replace("git+", "") const [parsed, ref] = raw.split("#") const name = typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git") return { name, url: parsed, ref, subdir } } if (url.startsWith("https://")) { const [parsed, ref] = url.split("#") const name = typeof source === "object" && source.name ? source.name : path.basename(parsed, ".git") return { name, url: parsed, ref, subdir } } throw new Error(`Cannot parse plugin source: ${formatSource(source)}`) } export function getGitCommit(pluginDir) { try { return execSync("git rev-parse HEAD", { cwd: pluginDir, encoding: "utf-8" }).trim() } catch { return "unknown" } } export function updateGlobalConfig(updates) { const json = readPluginsJson() if (!json) return false json.configuration = { ...json.configuration, ...updates } writePluginsJson(json) return true } export function configExists() { return fs.existsSync(CONFIG_YAML_PATH) || fs.existsSync(LEGACY_PLUGINS_JSON_PATH) } export function createConfigFromDefault() { const defaultData = readDefaultPluginsJson() if (!defaultData) { // No default available — create minimal config const minimal = { configuration: { pageTitle: "Quartz", enableSPA: true, enablePopovers: true, analytics: { provider: "plausible" }, locale: "en-US", baseUrl: "quartz.jzhao.xyz", ignorePatterns: ["private", "templates", ".obsidian"], theme: { 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: "#fff23688", }, }, }, }, plugins: [], layout: { groups: {}, byPageType: {} }, } writePluginsJson(minimal) return minimal } const { $schema, ...rest } = defaultData writePluginsJson(rest) return rest } const VALID_TEMPLATES = ["default", "obsidian", "ttrpg", "blog"] export function createConfigFromTemplate(templateName) { if (!VALID_TEMPLATES.includes(templateName)) { throw new Error( `Unknown template: ${templateName}. Valid templates: ${VALID_TEMPLATES.join(", ")}`, ) } const templatePath = path.join(TEMPLATES_DIR, `${templateName}.yaml`) const templateData = readFileAsData(templatePath) if (!templateData) { // Template file missing — fall back to default config creation return createConfigFromDefault() } const { $schema, ...rest } = templateData writePluginsJson(rest) return rest } /** * Resolves a user-facing plugin name (which may be an overridden name from config) * to the corresponding lockfile key (the original name at install time). * * This bridges the naming identity split between config YAML (which supports * source.name overrides) and the lockfile/disk (which are keyed by the original name). * * @param {string} name - The name the user provided (may be overridden or original) * @param {object|null} lockfile - The parsed lockfile * @param {object|null} pluginsJson - The parsed config YAML * @returns {string} The lockfile key that corresponds to this plugin */ export function resolveLockfileName(name, lockfile, pluginsJson) { // Direct match — no resolution needed if (lockfile?.plugins?.[name]) return name // Check if any config entry with this overridden name maps to a different lockfile key if (pluginsJson?.plugins) { const configEntry = pluginsJson.plugins.find( (e) => extractPluginName(e.source) === name || formatSource(e.source) === name, ) if (configEntry) { const url = getSourceUrl(configEntry.source) for (const [key, lock] of Object.entries(lockfile?.plugins ?? {})) { if ( lock.source === url || lock.source === formatSource(configEntry.source) || lock.resolved === url ) { return key } } } } return name } /** * Builds a map from lockfile keys to their overridden display names from config. * Returns entries only where the overridden name differs from the lockfile key. * * @param {object|null} lockfile - The parsed lockfile * @param {object|null} pluginsJson - The parsed config YAML * @returns {Map<string, string>} Map of lockfileKey → overriddenName */ export function getNameOverrides(lockfile, pluginsJson) { const overrides = new Map() if (!lockfile?.plugins || !pluginsJson?.plugins) return overrides for (const entry of pluginsJson.plugins) { const configName = extractPluginName(entry.source) const url = getSourceUrl(entry.source) for (const [lockKey, lock] of Object.entries(lockfile.plugins)) { if (lockKey === configName) break // no override, names match if ( lock.source === url || lock.source === formatSource(entry.source) || lock.resolved === url ) { overrides.set(lockKey, configName) break } } } return overrides } export const PLUGINS_JSON_PATH = CONFIG_YAML_PATH export const DEFAULT_PLUGINS_JSON_PATH = DEFAULT_CONFIG_YAML_PATH export { LOCKFILE_PATH, PLUGINS_DIR }