mirror of
https://github.com/jackyzha0/quartz.git
synced 2026-03-21 21:45:42 -05:00
feat: centralize native dep installation across plugins
Replace per-plugin npm install with a single aggregated npm install in the quartz root. Plugins declaring requiresInstall in their quartz manifest now have their peerDependencies collected and installed together, letting npm resolve compatible versions across plugins.
This commit is contained in:
parent
13a25cbfaf
commit
51b05e79e2
12
package-lock.json
generated
12
package-lock.json
generated
@ -93,9 +93,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/runtime": {
|
"node_modules/@emnapi/runtime": {
|
||||||
"version": "1.8.1",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
|
||||||
"integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
|
"integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -544,9 +544,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
"integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
parsePluginSource,
|
parsePluginSource,
|
||||||
installPlugin,
|
installPlugin,
|
||||||
|
installNativeDeps,
|
||||||
getPluginEntryPoint,
|
getPluginEntryPoint,
|
||||||
toFileUrl,
|
toFileUrl,
|
||||||
isLocalSource,
|
isLocalSource,
|
||||||
@ -236,12 +237,31 @@ export async function loadQuartzConfig(
|
|||||||
const enabledEntries = json.plugins.filter((e) => e.enabled)
|
const enabledEntries = json.plugins.filter((e) => e.enabled)
|
||||||
const manifests = new Map<string, PluginManifest>()
|
const manifests = new Map<string, PluginManifest>()
|
||||||
|
|
||||||
// Ensure all plugins are installed and collect manifests
|
// Ensure all plugins are installed and collect native deps
|
||||||
|
const allNativeDeps = new Map<string, Map<string, string>>()
|
||||||
for (const entry of enabledEntries) {
|
for (const entry of enabledEntries) {
|
||||||
try {
|
try {
|
||||||
const gitSpec = parsePluginSource(entry.source)
|
const gitSpec = parsePluginSource(entry.source)
|
||||||
await installPlugin(gitSpec, { verbose: false })
|
const result = await installPlugin(gitSpec, { verbose: false })
|
||||||
|
if (result.nativeDeps.size > 0) {
|
||||||
|
allNativeDeps.set(gitSpec.name, result.nativeDeps)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
styleText("red", `✗`) +
|
||||||
|
` Failed to install plugin: ${styleText("yellow", entry.source)}\n` +
|
||||||
|
` ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allNativeDeps.size > 0) {
|
||||||
|
installNativeDeps(allNativeDeps, { verbose: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect manifests (requires native deps to be installed first)
|
||||||
|
for (const entry of enabledEntries) {
|
||||||
|
try {
|
||||||
const manifest = await getManifest(entry.source)
|
const manifest = await getManifest(entry.source)
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
manifests.set(entry.source, manifest)
|
manifests.set(entry.source, manifest)
|
||||||
@ -249,7 +269,7 @@ export async function loadQuartzConfig(
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(
|
console.error(
|
||||||
styleText("red", `✗`) +
|
styleText("red", `✗`) +
|
||||||
` Failed to install plugin: ${styleText("yellow", entry.source)}\n` +
|
` Failed to load manifest: ${styleText("yellow", entry.source)}\n` +
|
||||||
` ${err instanceof Error ? err.message : String(err)}`,
|
` ${err instanceof Error ? err.message : String(err)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -118,43 +118,130 @@ function extractRepoName(url: string): string {
|
|||||||
return match ? match[1] : "unknown"
|
return match ? match[1] : "unknown"
|
||||||
}
|
}
|
||||||
|
|
||||||
async function installPluginDepsIfNeeded(
|
/**
|
||||||
pluginDir: string,
|
* Collect native (peer) dependencies from a plugin that declares requiresInstall.
|
||||||
pluginName: string,
|
*/
|
||||||
options: { verbose?: boolean },
|
function collectNativeDeps(pluginDir: string): Map<string, string> {
|
||||||
): Promise<void> {
|
const result = new Map<string, string>()
|
||||||
const pkgPath = path.join(pluginDir, "package.json")
|
const pkgPath = path.join(pluginDir, "package.json")
|
||||||
if (!fs.existsSync(pkgPath)) return
|
if (!fs.existsSync(pkgPath)) return result
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
|
||||||
const manifest = pkg.quartz ?? pkg.manifest ?? {}
|
const manifest = pkg.quartz ?? pkg.manifest ?? {}
|
||||||
if (!manifest.requiresInstall) return
|
if (!manifest.requiresInstall) return result
|
||||||
|
|
||||||
|
const peerDeps: Record<string, string> = pkg.peerDependencies ?? {}
|
||||||
|
for (const [name, range] of Object.entries(peerDeps)) {
|
||||||
|
// Skip shared externals that Quartz already provides
|
||||||
|
if (SHARED_EXTERNALS.some((prefix) => name.startsWith(prefix)) || name === "vfile") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.set(name, range)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore parse errors
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Install all collected native dependencies into the Quartz root with a single
|
||||||
|
* `npm install --no-save`. Lets npm resolve compatible versions across plugins.
|
||||||
|
*/
|
||||||
|
export function installNativeDeps(
|
||||||
|
nativeDeps: Map<string, Map<string, string>>,
|
||||||
|
options: { verbose?: boolean },
|
||||||
|
): void {
|
||||||
|
const merged = new Map<string, Map<string, string>>()
|
||||||
|
for (const [pluginName, deps] of nativeDeps) {
|
||||||
|
for (const [pkg, range] of deps) {
|
||||||
|
if (!merged.has(pkg)) {
|
||||||
|
merged.set(pkg, new Map())
|
||||||
|
}
|
||||||
|
merged.get(pkg)!.set(pluginName, range)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (merged.size === 0) return
|
||||||
|
|
||||||
|
const installArgs: string[] = []
|
||||||
|
for (const [pkg, pluginRanges] of merged) {
|
||||||
|
const ranges = [...pluginRanges.values()]
|
||||||
|
const uniqueRanges = [...new Set(ranges)]
|
||||||
|
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
console.log(styleText("cyan", `→`), `Installing native dependencies for ${pluginName}...`)
|
const sources = [...pluginRanges.entries()]
|
||||||
|
.map(([plugin, range]) => `${plugin} (${range})`)
|
||||||
|
.join(", ")
|
||||||
|
console.log(
|
||||||
|
styleText("cyan", `→`),
|
||||||
|
`Native dep ${styleText("bold", pkg)} required by: ${sources}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
execSync("npm install --omit=dev --ignore-scripts=false", {
|
if (uniqueRanges.length === 1) {
|
||||||
cwd: pluginDir,
|
installArgs.push(`${pkg}@${JSON.stringify(uniqueRanges[0])}`)
|
||||||
stdio: options.verbose ? "inherit" : "pipe",
|
} else {
|
||||||
timeout: 60_000,
|
if (options.verbose) {
|
||||||
})
|
console.warn(
|
||||||
} catch {
|
styleText("yellow", `⚠`),
|
||||||
console.warn(
|
`Multiple version ranges for ${pkg}: ${uniqueRanges.join(", ")}. npm will attempt to resolve a compatible version.`,
|
||||||
styleText("yellow", `⚠`),
|
)
|
||||||
`Failed to install dependencies for ${pluginName}. Native features may not work.`,
|
}
|
||||||
|
// Use first range; npm will fail if truly incompatible
|
||||||
|
installArgs.push(`${pkg}@${JSON.stringify(uniqueRanges[0])}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (installArgs.length === 0) return
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(
|
||||||
|
styleText("cyan", `→`),
|
||||||
|
`Installing ${installArgs.length} native package(s) into Quartz root...`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
execSync(`npm install --no-save ${installArgs.join(" ")}`, {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
stdio: options.verbose ? "inherit" : "pipe",
|
||||||
|
timeout: 120_000,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (options.verbose) {
|
||||||
|
console.log(
|
||||||
|
styleText("green", `✓`),
|
||||||
|
`Installed native dependencies: ${[...merged.keys()].join(", ")}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
|
console.error(
|
||||||
|
styleText("red", `✗`),
|
||||||
|
`Failed to install native dependencies. This may indicate incompatible version ranges across plugins.\n` +
|
||||||
|
` Packages: ${[...merged.keys()].join(", ")}\n` +
|
||||||
|
` Error: ${message}`,
|
||||||
|
)
|
||||||
|
throw new Error(`Native dependency installation failed: ${message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PluginInstallResult {
|
||||||
|
pluginDir: string
|
||||||
|
nativeDeps: Map<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a plugin from a Git repository, or symlink a local plugin.
|
* Install a plugin from a Git repository, or symlink a local plugin.
|
||||||
|
* Returns the plugin directory and any native dependencies it requires.
|
||||||
*/
|
*/
|
||||||
export async function installPlugin(
|
export async function installPlugin(
|
||||||
spec: GitPluginSpec,
|
spec: GitPluginSpec,
|
||||||
options: { verbose?: boolean; force?: boolean } = {},
|
options: { verbose?: boolean; force?: boolean } = {},
|
||||||
): Promise<string> {
|
): Promise<PluginInstallResult> {
|
||||||
const pluginDir = path.join(PLUGINS_CACHE_DIR, spec.name)
|
const pluginDir = path.join(PLUGINS_CACHE_DIR, spec.name)
|
||||||
|
|
||||||
// Local source: symlink instead of clone
|
// Local source: symlink instead of clone
|
||||||
@ -171,7 +258,7 @@ export async function installPlugin(
|
|||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
console.log(styleText("cyan", `→`), `Plugin ${spec.name} already linked`)
|
console.log(styleText("cyan", `→`), `Plugin ${spec.name} already linked`)
|
||||||
}
|
}
|
||||||
return pluginDir
|
return { pluginDir, nativeDeps: collectNativeDeps(pluginDir) }
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// stat failed, recreate
|
// stat failed, recreate
|
||||||
@ -204,7 +291,7 @@ export async function installPlugin(
|
|||||||
console.log(styleText("green", `✓`), `Linked ${spec.name}`)
|
console.log(styleText("green", `✓`), `Linked ${spec.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pluginDir
|
return { pluginDir, nativeDeps: collectNativeDeps(pluginDir) }
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git source: clone
|
// Git source: clone
|
||||||
@ -216,7 +303,7 @@ export async function installPlugin(
|
|||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
console.log(styleText("cyan", `→`), `Plugin ${spec.name} already installed`)
|
console.log(styleText("cyan", `→`), `Plugin ${spec.name} already installed`)
|
||||||
}
|
}
|
||||||
return pluginDir
|
return { pluginDir, nativeDeps: collectNativeDeps(pluginDir) }
|
||||||
} catch {
|
} catch {
|
||||||
// If git operations fail, re-clone
|
// If git operations fail, re-clone
|
||||||
}
|
}
|
||||||
@ -244,13 +331,11 @@ export async function installPlugin(
|
|||||||
noCheckout: false,
|
noCheckout: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
await installPluginDepsIfNeeded(pluginDir, spec.name, options)
|
|
||||||
|
|
||||||
if (options.verbose) {
|
if (options.verbose) {
|
||||||
console.log(styleText("green", `✓`), `Installed ${spec.name}`)
|
console.log(styleText("green", `✓`), `Installed ${spec.name}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pluginDir
|
return { pluginDir, nativeDeps: collectNativeDeps(pluginDir) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,18 +346,26 @@ export async function installPlugins(
|
|||||||
options: { verbose?: boolean; force?: boolean } = {},
|
options: { verbose?: boolean; force?: boolean } = {},
|
||||||
): Promise<Map<string, string>> {
|
): Promise<Map<string, string>> {
|
||||||
const installed = new Map<string, string>()
|
const installed = new Map<string, string>()
|
||||||
|
const allNativeDeps = new Map<string, Map<string, string>>()
|
||||||
|
|
||||||
for (const source of sources) {
|
for (const source of sources) {
|
||||||
try {
|
try {
|
||||||
const spec = typeof source === "string" ? parsePluginSource(source) : source
|
const spec = typeof source === "string" ? parsePluginSource(source) : source
|
||||||
const pluginDir = await installPlugin(spec, options)
|
const result = await installPlugin(spec, options)
|
||||||
installed.set(spec.name, pluginDir)
|
installed.set(spec.name, result.pluginDir)
|
||||||
|
if (result.nativeDeps.size > 0) {
|
||||||
|
allNativeDeps.set(spec.name, result.nativeDeps)
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error)
|
const message = error instanceof Error ? error.message : String(error)
|
||||||
console.error(styleText("red", `✗`), `Failed to install plugin: ${message}`)
|
console.error(styleText("red", `✗`), `Failed to install plugin: ${message}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allNativeDeps.size > 0) {
|
||||||
|
installNativeDeps(allNativeDeps, options)
|
||||||
|
}
|
||||||
|
|
||||||
await regeneratePluginIndex(options)
|
await regeneratePluginIndex(options)
|
||||||
|
|
||||||
return installed
|
return installed
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user