163 lines
5.7 KiB
JavaScript
163 lines
5.7 KiB
JavaScript
#!/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.`,
|
|
);
|
|
}
|