feat(plugin): support enterprise extension (#861)
This commit is contained in:
162
scripts/generate-ext-bridge.mjs
Normal file
162
scripts/generate-ext-bridge.mjs
Normal file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Generates extension bridge files based on clawx-extensions.json and
|
||||
* which packages are actually installed in node_modules.
|
||||
*
|
||||
* Outputs:
|
||||
* electron/extensions/_ext-bridge.generated.ts (main process)
|
||||
* src/extensions/_ext-bridge.generated.ts (renderer)
|
||||
*
|
||||
* Both files are .gitignore'd. When no external extensions are installed,
|
||||
* they export no-op functions so the core compiles cleanly.
|
||||
*
|
||||
* Usage: node scripts/generate-ext-bridge.mjs
|
||||
*/
|
||||
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
||||
import { resolve, dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = resolve(__dirname, '..');
|
||||
|
||||
const MANIFEST_PATH = resolve(ROOT, 'clawx-extensions.json');
|
||||
const MAIN_OUT = resolve(ROOT, 'electron/extensions/_ext-bridge.generated.ts');
|
||||
const RENDERER_OUT = resolve(ROOT, 'src/extensions/_ext-bridge.generated.ts');
|
||||
|
||||
function readManifest() {
|
||||
if (!existsSync(MANIFEST_PATH)) return { extensions: {} };
|
||||
try {
|
||||
return JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'));
|
||||
} catch {
|
||||
return { extensions: {} };
|
||||
}
|
||||
}
|
||||
|
||||
function isExternalId(id) {
|
||||
return id && !id.startsWith('builtin/');
|
||||
}
|
||||
|
||||
function resolvePackageName(extensionId) {
|
||||
// Convention: extension IDs like "@scope/pkg/sub" map to package "@scope/pkg"
|
||||
// IDs like "@scope/pkg" map to package "@scope/pkg"
|
||||
const parts = extensionId.split('/');
|
||||
if (parts[0].startsWith('@') && parts.length >= 2) {
|
||||
return parts.slice(0, 2).join('/');
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
function isPackageInstalled(pkgName) {
|
||||
return existsSync(resolve(ROOT, 'node_modules', ...pkgName.split('/')));
|
||||
}
|
||||
|
||||
function generateMainBridge(manifest) {
|
||||
const externalMain = (manifest.extensions?.main ?? []).filter(isExternalId);
|
||||
const installedExts = externalMain.filter((id) => isPackageInstalled(resolvePackageName(id)));
|
||||
|
||||
if (installedExts.length === 0) {
|
||||
return [
|
||||
'// Auto-generated — no external main-process extensions installed.',
|
||||
'// To add extensions, configure clawx-extensions.json and link the package.',
|
||||
'export function loadExternalMainExtensions(): void { /* no-op */ }',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'// Auto-generated by scripts/generate-ext-bridge.mjs — do not edit.',
|
||||
"import { extensionRegistry } from './registry';",
|
||||
'',
|
||||
];
|
||||
|
||||
installedExts.forEach((id, i) => {
|
||||
const pkg = resolvePackageName(id);
|
||||
const subpath = id.slice(pkg.length + 1); // e.g. "enterprise-auth"
|
||||
const importPath = subpath ? `${pkg}/${subpath}` : pkg;
|
||||
const factoryName = `ext${i}`;
|
||||
lines.push(`import { ${guessFactoryExport(subpath || id)} as ${factoryName} } from '${importPath}';`);
|
||||
});
|
||||
|
||||
lines.push('');
|
||||
lines.push('export function loadExternalMainExtensions(): void {');
|
||||
installedExts.forEach((_id, i) => {
|
||||
lines.push(` const e${i} = ext${i}();`);
|
||||
lines.push(` if (e${i}) extensionRegistry.register(e${i});`);
|
||||
});
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function generateRendererBridge(manifest) {
|
||||
const externalRenderer = (manifest.extensions?.renderer ?? []).filter(isExternalId);
|
||||
const installedExts = externalRenderer.filter((id) => isPackageInstalled(resolvePackageName(id)));
|
||||
|
||||
if (installedExts.length === 0) {
|
||||
return [
|
||||
'// Auto-generated — no external renderer extensions installed.',
|
||||
'// To add extensions, configure clawx-extensions.json and link the package.',
|
||||
'export function loadExternalRendererExtensions(): void { /* no-op */ }',
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
const lines = [
|
||||
'// Auto-generated by scripts/generate-ext-bridge.mjs — do not edit.',
|
||||
"import { rendererExtensionRegistry } from './registry';",
|
||||
'',
|
||||
];
|
||||
|
||||
installedExts.forEach((id, i) => {
|
||||
const pkg = resolvePackageName(id);
|
||||
const subpath = id.slice(pkg.length + 1);
|
||||
const importPath = subpath ? `${pkg}/${subpath}` : pkg;
|
||||
const factoryName = `ext${i}`;
|
||||
lines.push(`import { ${guessFactoryExport(subpath || id)} as ${factoryName} } from '${importPath}';`);
|
||||
});
|
||||
|
||||
lines.push('');
|
||||
lines.push('export function loadExternalRendererExtensions(): void {');
|
||||
installedExts.forEach((_id, i) => {
|
||||
lines.push(` const e${i} = ext${i}();`);
|
||||
lines.push(` if (e${i}) rendererExtensionRegistry.register(e${i});`);
|
||||
});
|
||||
lines.push('}');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function guessFactoryExport(subpath) {
|
||||
// "enterprise-auth" → "createEnterpriseAuthExtension"
|
||||
// "enterprise-ui" → "createEnterpriseUIExtension"
|
||||
// "skillshub-marketplace" → "createSkillshubMarketplaceExtension"
|
||||
const camel = subpath
|
||||
.replace(/[^a-zA-Z0-9]+(.)/g, (_, c) => c.toUpperCase())
|
||||
.replace(/^./, (c) => c.toUpperCase());
|
||||
return `create${camel}Extension`;
|
||||
}
|
||||
|
||||
// ─── Main ───
|
||||
const manifest = readManifest();
|
||||
|
||||
mkdirSync(dirname(MAIN_OUT), { recursive: true });
|
||||
mkdirSync(dirname(RENDERER_OUT), { recursive: true });
|
||||
|
||||
writeFileSync(MAIN_OUT, generateMainBridge(manifest));
|
||||
writeFileSync(RENDERER_OUT, generateRendererBridge(manifest));
|
||||
|
||||
const mainExts = (manifest.extensions?.main ?? []).filter(isExternalId);
|
||||
const rendererExts = (manifest.extensions?.renderer ?? []).filter(isExternalId);
|
||||
const totalExternal = mainExts.length + rendererExts.length;
|
||||
|
||||
if (totalExternal === 0) {
|
||||
console.log('[ext-bridge] No external extensions configured — generated empty stubs.');
|
||||
} else {
|
||||
const installedMain = mainExts.filter((id) => isPackageInstalled(resolvePackageName(id)));
|
||||
const installedRenderer = rendererExts.filter((id) => isPackageInstalled(resolvePackageName(id)));
|
||||
console.log(
|
||||
`[ext-bridge] Generated bridges: ${installedMain.length}/${mainExts.length} main, ${installedRenderer.length}/${rendererExts.length} renderer extensions resolved.`,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user