From ec705989e375f2953cbf88345814031945ae3a1a Mon Sep 17 00:00:00 2001 From: Jenny Ferries Date: Thu, 6 Aug 2026 09:18:27 -0700 Subject: [PATCH] fix(plugin-validation): allow parent-bundled extensions Accept reusable canvas sources that are referenced by an existing parent plugin, matching the post-#2546 scaffolding guidance, while continuing to reject orphaned sources. Cover parent-only, standalone, and orphaned registrations. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 52be9c67-3ae4-4610-93d0-fe0b7ab95ccb --- eng/validate-plugins.mjs | 42 ++++++++++++++++++++++++----------- eng/validate-plugins.test.mjs | 36 ++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 13 deletions(-) create mode 100644 eng/validate-plugins.test.mjs diff --git a/eng/validate-plugins.mjs b/eng/validate-plugins.mjs index 38d0304a..1c01eaff 100755 --- a/eng/validate-plugins.mjs +++ b/eng/validate-plugins.mjs @@ -2,6 +2,7 @@ import fs from "fs"; import path from "path"; +import { fileURLToPath } from "url"; import { ROOT_FOLDER } from "./constants.mjs"; import { readExternalPlugins } from "./external-plugin-validation.mjs"; import { validateLicenseField } from "./lib/license.mjs"; @@ -334,7 +335,11 @@ function validateExtensionScreenshotPath(extensionDir, pathValue, fieldName, err } // Main validation function -function validatePlugins() { +export function isReusableExtensionRegistered(extensionName, pluginDirectoryNames, referencedExtensionNames) { + return pluginDirectoryNames.has(extensionName) || referencedExtensionNames.has(extensionName); +} + +export function validatePlugins() { const pluginDirs = fs.existsSync(PLUGINS_DIR) ? fs.readdirSync(PLUGINS_DIR, { withFileTypes: true }) .filter((d) => d.isDirectory()) @@ -350,6 +355,8 @@ function validatePlugins() { let hasErrors = false; const seenNames = new Set(); const localPluginNames = []; + const pluginDirectoryNames = new Set(pluginDirs); + const referencedExtensionNames = new Set(); for (const dir of pluginDirs) { console.log(`Validating ${dir}...`); @@ -377,12 +384,20 @@ function validatePlugins() { localPluginNames.push(plugin.name); } } + + const extensionReferences = plugin?.extensions?.[AWESOME_COPILOT_NAMESPACE]?.extensions; + if (Array.isArray(extensionReferences)) { + for (const reference of extensionReferences) { + if (typeof reference === "string" && reference.startsWith("./extensions/")) { + referencedExtensionNames.add(reference.replace(/^\.\/extensions\//, "").replace(/\/$/, "")); + } + } + } } for (const dir of getExtensionFolderNames()) { - const pluginJsonPath = path.join(PLUGINS_DIR, dir, "plugin.json"); - if (!fs.existsSync(pluginJsonPath)) { - console.error(`āŒ extension ${dir}: missing plugin manifest at plugins/${dir}/plugin.json`); + if (!isReusableExtensionRegistered(dir, pluginDirectoryNames, referencedExtensionNames)) { + console.error(`āŒ extension ${dir}: must be referenced by a plugin or have a standalone manifest at plugins/${dir}/plugin.json`); hasErrors = true; } } @@ -410,15 +425,16 @@ function validatePlugins() { return !hasErrors; } -// Run validation -try { - const isValid = validatePlugins(); - if (!isValid) { - console.error("\nāŒ Plugin validation failed"); +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + try { + const isValid = validatePlugins(); + if (!isValid) { + console.error("\nāŒ Plugin validation failed"); + process.exit(1); + } + console.log("\nšŸŽ‰ Plugin validation passed"); + } catch (error) { + console.error(`Error during validation: ${error.message}`); process.exit(1); } - console.log("\nšŸŽ‰ Plugin validation passed"); -} catch (error) { - console.error(`Error during validation: ${error.message}`); - process.exit(1); } diff --git a/eng/validate-plugins.test.mjs b/eng/validate-plugins.test.mjs new file mode 100644 index 00000000..e1d2fd3d --- /dev/null +++ b/eng/validate-plugins.test.mjs @@ -0,0 +1,36 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { isReusableExtensionRegistered } from "./validate-plugins.mjs"; + +test("accepts a reusable extension bundled only by a parent plugin", () => { + assert.equal( + isReusableExtensionRegistered( + "daily-focus-board", + new Set(["ember"]), + new Set(["daily-focus-board"]) + ), + true + ); +}); + +test("accepts a same-named standalone extension plugin", () => { + assert.equal( + isReusableExtensionRegistered( + "daily-focus-board", + new Set(["daily-focus-board"]), + new Set() + ), + true + ); +}); + +test("rejects an orphaned reusable extension", () => { + assert.equal( + isReusableExtensionRegistered( + "daily-focus-board", + new Set(["ember"]), + new Set() + ), + false + ); +});