diff --git a/quartz/cli/plugin-git-handlers.js b/quartz/cli/plugin-git-handlers.js index 81d1ef016dd3c..b010d2d9b9976 100644 --- a/quartz/cli/plugin-git-handlers.js +++ b/quartz/cli/plugin-git-handlers.js @@ -174,6 +174,16 @@ function linkPeerPlugins(pluginDir) { } } +/** + * Whether a plugin's previously-installed lockfile record no longer matches + * its current config source (e.g. a pinned ref was bumped), meaning the + * on-disk plugin directory is stale and must be reinstalled rather than + * reused as-is. + */ +export function pluginSourceChanged(priorEntry, currentSource) { + return priorEntry !== undefined && priorEntry.source !== currentSource +} + /** * Search installed plugins for one whose package.json "name" matches the given * npm package name (e.g. "@quartz-community/bases-page"). @@ -506,7 +516,13 @@ export async function handlePluginInstallUnified({ .filter((entry) => { const name = extractPluginName(entry.source) const pluginDir = path.join(PLUGINS_DIR, name) - if (lockfile.plugins[name] && fs.existsSync(pluginDir)) return false + const priorEntry = lockfile.plugins[name] + if ( + !pluginSourceChanged(priorEntry, entry.source) && + priorEntry && + fs.existsSync(pluginDir) + ) + return false const src = getSourceUrl(entry.source) return ( src.startsWith("github:") || @@ -575,6 +591,17 @@ export async function handlePluginInstallUnified({ try { const { name, url, ref, local, subdir } = parseGitSource(entry.source) const pluginDir = path.join(PLUGINS_DIR, name) + const priorEntry = lockfile.plugins[name] + + if (fs.existsSync(pluginDir) && pluginSourceChanged(priorEntry, entry.source)) { + console.log( + styleText( + "yellow", + `⚠ ${name} source changed (${formatSource(priorEntry.source)} → ${formatSource(entry.source)}), reinstalling`, + ), + ) + fs.rmSync(pluginDir, { recursive: true, force: true }) + } if (fs.existsSync(pluginDir)) { if (local) { diff --git a/quartz/cli/plugin-git-handlers.test.js b/quartz/cli/plugin-git-handlers.test.js new file mode 100644 index 0000000000000..bd548c391f651 --- /dev/null +++ b/quartz/cli/plugin-git-handlers.test.js @@ -0,0 +1,30 @@ +import test, { describe } from "node:test" +import assert from "node:assert" +import { pluginSourceChanged } from "./plugin-git-handlers.js" + +describe("pluginSourceChanged", () => { + test("is false when there is no prior lockfile record", () => { + // First-ever install: nothing to compare against, so this alone must not + // force a reinstall of an unrelated pre-existing directory. + assert.strictEqual(pluginSourceChanged(undefined, "github:owner/repo#main"), false) + }) + + test("is false when the source is unchanged", () => { + const priorEntry = { source: "github:owner/repo#main", resolved: "...", commit: "abc123" } + assert.strictEqual(pluginSourceChanged(priorEntry, "github:owner/repo#main"), false) + }) + + test("is true when the pinned ref changed", () => { + // This is the case `plugin install --from-config` used to miss entirely: + // the directory and lockfile entry both already exist, so the plugin was + // treated as "already installed" and never re-cloned even though its + // configured ref moved to a different branch. + const priorEntry = { source: "github:owner/repo#old-branch", resolved: "...", commit: "abc123" } + assert.strictEqual(pluginSourceChanged(priorEntry, "github:owner/repo#new-branch"), true) + }) + + test("is true when the repo itself changed", () => { + const priorEntry = { source: "github:owner/old-repo", resolved: "...", commit: "abc123" } + assert.strictEqual(pluginSourceChanged(priorEntry, "github:owner/new-repo"), true) + }) +})