feat: migrate default config and templates to npm specifiers

- 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)
This commit is contained in:
saberzero1
2026-07-23 01:29:25 +02:00
parent ab8e77392c
commit 97eea3ee34
11 changed files with 297 additions and 530 deletions
+50 -8
View File
@@ -4,6 +4,7 @@ import { execSync } from "child_process"
import git from "isomorphic-git"
import http from "isomorphic-git/http/node"
import { styleText } from "util"
import { createRequire } from "module"
import { pathToFileURL } from "url"
import { PluginSource } from "./types"
@@ -916,9 +917,11 @@ export function validatePluginExternals(
}
}
export async function regeneratePluginIndex(options: { verbose?: boolean } = {}): Promise<void> {
export async function regeneratePluginIndex(
options: { verbose?: boolean; npmPackages?: string[] } = {},
): Promise<void> {
if (!fs.existsSync(PLUGINS_CACHE_DIR)) {
return
fs.mkdirSync(PLUGINS_CACHE_DIR, { recursive: true })
}
const pluginDirs = fs.readdirSync(PLUGINS_CACHE_DIR).filter((name) => {
@@ -927,10 +930,12 @@ export async function regeneratePluginIndex(options: { verbose?: boolean } = {})
})
// Phase 1: Collect all exports per plugin, detect conflicts
// importPath maps plugin key → the import specifier used in generated index.ts
const pluginExports = new Map<
string,
{ overridable: string[]; passthrough: string[]; types: string[] }
>()
const importPath = new Map<string, string>()
const nameCount = new Map<string, number>()
for (const pluginName of pluginDirs) {
@@ -954,6 +959,45 @@ export async function regeneratePluginIndex(options: { verbose?: boolean } = {})
if (overridable.length > 0 || passthrough.length > 0 || types.length > 0) {
pluginExports.set(pluginName, { overridable, passthrough, types })
importPath.set(pluginName, `./${pluginName}`)
for (const n of [...overridable, ...passthrough]) {
nameCount.set(n, (nameCount.get(n) ?? 0) + 1)
}
}
}
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
}
if (!distIndex || !fs.existsSync(distIndex)) {
if (options.verbose) {
console.log(styleText("yellow", ``), `Skipping npm package ${npmPkg}: no dist/index.d.ts`)
}
continue
}
const dtsContent = fs.readFileSync(distIndex, "utf-8")
const exportedNames = parseExportsFromDts(dtsContent)
const named = exportedNames.filter((e) => !e.startsWith("type "))
const types = exportedNames.filter((e) => e.startsWith("type ")).map((e) => e.slice(5))
const overridable = named.filter((n) => isOverridableExport(n, dtsContent))
const passthrough = named.filter((n) => !isOverridableExport(n, dtsContent))
const key = npmPkg.replace(/^@/, "").replace(/\//g, "__")
if (overridable.length > 0 || passthrough.length > 0 || types.length > 0) {
pluginExports.set(key, { overridable, passthrough, types })
importPath.set(key, npmPkg)
for (const n of [...overridable, ...passthrough]) {
nameCount.set(n, (nameCount.get(n) ?? 0) + 1)
}
@@ -966,19 +1010,17 @@ export async function regeneratePluginIndex(options: { verbose?: boolean } = {})
lines.push(`import { componentRegistry } from "../../quartz/components/registry"`)
lines.push("")
// Type re-exports
for (const [pluginName, { types }] of pluginExports) {
for (const [pluginKey, { types }] of pluginExports) {
if (types.length > 0) {
lines.push(`export type { ${types.join(", ")} } from "./${pluginName}"`)
lines.push(`export type { ${types.join(", ")} } from "${importPath.get(pluginKey)}"`)
}
}
// Direct re-exports for non-overridable values (constants, utility functions, etc.)
for (const [pluginName, { passthrough }] of pluginExports) {
for (const [pluginKey, { passthrough }] of pluginExports) {
if (passthrough.length === 0) continue
const unique = passthrough.filter((n) => (nameCount.get(n) ?? 0) === 1)
if (unique.length > 0) {
lines.push(`export { ${unique.join(", ")} } from "./${pluginName}"`)
lines.push(`export { ${unique.join(", ")} } from "${importPath.get(pluginKey)}"`)
}
}
lines.push("")
+21 -10
View File
@@ -2,7 +2,7 @@
import fs from "fs"
import path from "path"
import YAML from "yaml"
import { installPlugins, parsePluginSource } from "./gitLoader.js"
import { installPlugins, parsePluginSource, regeneratePluginIndex } from "./gitLoader.js"
import type { PluginSource, QuartzPluginsJson } from "./types.js"
function resolveConfigPath(): string {
@@ -57,19 +57,30 @@ async function main() {
.map((source: PluginSource) => parsePluginSource(source))
.filter((spec) => !spec.npmPackage)
if (specs.length === 0) {
console.log("No external Git plugins to install.")
const npmSpecs = externalPlugins
.map((source: PluginSource) => parsePluginSource(source))
.filter((spec) => spec.npmPackage)
if (specs.length === 0 && npmSpecs.length === 0) {
console.log("No external plugins to install.")
return
}
console.log(`Installing ${specs.length} plugin(s) from Git...`)
const installed = await installPlugins(specs, { verbose: true })
if (specs.length > 0) {
console.log(`Installing ${specs.length} plugin(s) from Git...`)
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)
if (installed.size === specs.length) {
console.log("✓ All Git plugins installed successfully")
} else {
console.error(`✗ Only ${installed.size}/${specs.length} Git plugins installed`)
process.exit(1)
}
}
if (npmSpecs.length > 0) {
console.log(`Found ${npmSpecs.length} npm plugin(s), regenerating plugin index...`)
await regeneratePluginIndex({ verbose: true, npmPackages: npmSpecs.map((s) => s.name) })
}
}
+1 -1
View File
@@ -51,7 +51,7 @@ export interface PluginManifest {
keywords?: string[]
category?: PluginCategory | PluginCategory[]
quartzVersion?: string
/** Plugin sources this plugin depends on (e.g., "github:quartz-community/crawl-links") */
/** Plugin sources this plugin depends on (e.g., "@quartz-community/crawl-links") */
dependencies?: string[]
/** Default numeric execution order (0-100 convention, lower = runs first). Defaults to 50. */
defaultOrder?: number