Fix extension materialization to move bundles into container (#2339)

* Migrate extension plugin materialization layout

Materialize extension plugins into a dedicated extensions/ container, validate the new manifest convention, and bump extension plugin versions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Keep extension manifests source-compatible

Restore source extension manifests to "extensions": "." while preserving materialization-time rewrite to "extensions" in distribution output.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Validate canvas extension layout for external submissions

Add intake and quality-gate checks for canvas-tagged external plugins so they must include extensions/extension.mjs and optional manifest extensions is validated when present.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Fix plugin clean extension pass and typo guard text

Declare EXTENSIONS_DIR in clean-materialized-plugins and run extension cleanup once after plugin cleanup. Also normalize misspelled-key detection strings to satisfy spelling checks without changing validation behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Proper codespell fix

* Separate canvas structure quality gate status

Track canvas structure as its own gate status and output, include it in aggregate summaries, and enforce Git object types so extensions/ is a tree and extensions/extension.mjs is a blob.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Move extension bundles during materialization

Change extension-plugin materialization to move root bundle entries into extensions/ instead of copying them, and assert originals are removed in test coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Nest materialized extension bundles by plugin name

Materialize extension plugins into extensions/<plugin-name>/... (moved entries) so resulting paths are duplicated by design, and bump extension plugin versions to the next patch release.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Fix extension materialization publish and cleanup

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1

* Preserve root extension logo asset

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d26008fb-9928-4ba7-b7c8-36f35320f7c1
This commit is contained in:
Aaron Powell
2026-07-18 10:49:27 +10:00
committed by GitHub
parent 71df97432a
commit 26fe2d126b
24 changed files with 222 additions and 63 deletions
+41 -11
View File
@@ -24,15 +24,34 @@ function copyDirRecursive(src, dest) {
}
}
function copyEntryRecursive(srcPath, destPath) {
function moveEntry(srcPath, destPath) {
fs.mkdirSync(path.dirname(destPath), { recursive: true });
try {
fs.renameSync(srcPath, destPath);
return;
} catch (error) {
if (error?.code !== "EXDEV") {
throw error;
}
}
const stats = fs.statSync(srcPath);
if (stats.isDirectory()) {
copyDirRecursive(srcPath, destPath);
fs.rmSync(srcPath, { recursive: true, force: true });
return;
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(srcPath, destPath);
fs.rmSync(srcPath, { force: true });
}
function isRelativeAssetPath(assetPath) {
return typeof assetPath === "string" &&
assetPath.length > 0 &&
!/^(?:[a-z][a-z0-9+.-]*:)?\/\//i.test(assetPath) &&
!assetPath.startsWith("data:") &&
!path.isAbsolute(assetPath);
}
/**
@@ -61,7 +80,7 @@ function resolveSource(relPath) {
export function materializeExtensionPlugin(extensionPath) {
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
return { copiedEntries: 0, manifestUpdated: false, skipped: true };
return { movedEntries: 0, manifestUpdated: false, skipped: true };
}
let metadata;
@@ -72,20 +91,31 @@ export function materializeExtensionPlugin(extensionPath) {
}
const extensionContainerPath = path.join(extensionPath, "extensions");
const extensionBundlePath = path.join(extensionContainerPath, path.basename(extensionPath));
fs.rmSync(extensionContainerPath, { recursive: true, force: true });
fs.mkdirSync(extensionContainerPath, { recursive: true });
fs.mkdirSync(extensionBundlePath, { recursive: true });
let copiedEntries = 0;
let movedEntries = 0;
for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) {
if (entry.name === ".github" || entry.name === "extensions") {
continue;
}
copyEntryRecursive(
moveEntry(
path.join(extensionPath, entry.name),
path.join(extensionContainerPath, entry.name)
path.join(extensionBundlePath, entry.name)
);
copiedEntries++;
movedEntries++;
}
if (isRelativeAssetPath(metadata.logo)) {
const normalizedLogoPath = metadata.logo.replace(/\\/g, "/").replace(/^\.\//, "");
const bundledLogoPath = path.join(extensionBundlePath, normalizedLogoPath);
if (fs.existsSync(bundledLogoPath)) {
const rootLogoPath = path.join(extensionPath, normalizedLogoPath);
fs.mkdirSync(path.dirname(rootLogoPath), { recursive: true });
fs.copyFileSync(bundledLogoPath, rootLogoPath);
}
}
let manifestUpdated = false;
@@ -97,7 +127,7 @@ export function materializeExtensionPlugin(extensionPath) {
fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8");
}
return { copiedEntries, manifestUpdated, skipped: false };
return { movedEntries, manifestUpdated, skipped: false };
}
function materializePlugins() {
@@ -261,8 +291,8 @@ function materializePlugins() {
}
totalExtensionPlugins++;
totalExtensionPluginEntries += result.copiedEntries;
console.log(`${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`);
totalExtensionPluginEntries += result.movedEntries;
console.log(`${dirName}: materialized extension bundle into ./extensions (${result.movedEntries} entries)`);
} catch (err) {
console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`);
errors++;