Procházet zdrojové kódy

Load Anchor adapter dynamically in CLI (#771)

This PR removes the `nodes-from-anchor` dependency from the CLI package and, instead, offers the user to install if and only if the used IDL is identified to be an Anchor IDL.

This decouples the CLI from this package making it possible to add it to the main `codama` library without any linked versioning issues, but also allows the user to control the exact version they want to use for their Anchor adapter.
Loris Leiva před 3 měsíci
rodič
revize
a11fcdc489

+ 5 - 0
.changeset/cyan-news-march.md

@@ -0,0 +1,5 @@
+---
+'@codama/cli': minor
+---
+
+The CLI now offers to install `@codama/nodes-from-anchor` dynamically when an Anchor IDL is detected instead of bundling it in the CLI package. This allows users to have more control over the version of their Anchor adapter.

+ 0 - 1
packages/cli/package.json

@@ -44,7 +44,6 @@
     },
     "dependencies": {
         "@codama/nodes": "workspace:*",
-        "@codama/nodes-from-anchor": "workspace:*",
         "@codama/visitors": "workspace:*",
         "@codama/visitors-core": "workspace:*",
         "commander": "^14.0.0",

+ 15 - 0
packages/cli/src/commands/init.ts

@@ -5,7 +5,9 @@ import prompts, { PromptType } from 'prompts';
 import { Config, ScriptConfig, ScriptName } from '../config';
 import {
     canRead,
+    importModuleItem,
     installMissingDependencies,
+    isRootNode,
     logBanner,
     logSuccess,
     PROMPT_OPTIONS,
@@ -120,7 +122,9 @@ async function getPromptResult(
         PROMPT_OPTIONS,
     );
 
+    const isAnchor = await isAnchorIdl(result.idlPath);
     await installMissingDependencies(`Your configuration requires additional dependencies.`, [
+        ...(isAnchor ? ['@codama/nodes-from-anchor'] : []),
         ...(result.scripts.includes('js') ? ['@codama/renderers-js'] : []),
         ...(result.scripts.includes('rust') ? ['@codama/renderers-rust'] : []),
     ]);
@@ -194,3 +198,14 @@ function getContentForGill(result: PromptResult): string {
         `export default createCodamaConfig({\n${attributesString}});\n`
     );
 }
+
+async function isAnchorIdl(idlPath: string): Promise<boolean> {
+    const resolvedIdlPath = resolveRelativePath(idlPath);
+    if (!(await canRead(resolvedIdlPath))) return false;
+    try {
+        const idlContent = await importModuleItem('IDL', resolvedIdlPath);
+        return !isRootNode(idlContent);
+    } catch {
+        return false;
+    }
+}

+ 1 - 1
packages/cli/src/parsedConfig.ts

@@ -45,7 +45,7 @@ async function parseConfig(
 ): Promise<ParsedConfig> {
     const idlPath = parseIdlPath(config, configPath, options);
     const idlContent = await importModuleItem('IDL', idlPath);
-    const rootNode = getRootNodeFromIdl(idlContent);
+    const rootNode = await getRootNodeFromIdl(idlContent);
     const scripts = parseScripts(config.scripts ?? {}, configPath);
     const visitors = (config.before ?? []).map((v, i) => parseVisitorConfig(v, configPath, i, null));
 

+ 19 - 3
packages/cli/src/utils/nodes.ts

@@ -1,14 +1,30 @@
 import type { RootNode } from '@codama/nodes';
-import { type AnchorIdl, rootNodeFromAnchor } from '@codama/nodes-from-anchor';
 
-export function getRootNodeFromIdl(idl: unknown): RootNode {
+import { importModuleItem } from './import';
+import { installMissingDependencies } from './packageInstall';
+
+export async function getRootNodeFromIdl(idl: unknown): Promise<RootNode> {
     if (typeof idl !== 'object' || idl === null) {
         throw new Error('Unexpected IDL content. Expected an object, got ' + typeof idl);
     }
     if (isRootNode(idl)) {
         return idl;
     }
-    return rootNodeFromAnchor(idl as AnchorIdl);
+
+    const hasNodesFromAnchor = await installMissingDependencies(
+        'Anchor IDL detected. Additional dependencies are required to process Anchor IDLs.',
+        ['@codama/nodes-from-anchor'],
+    );
+    if (!hasNodesFromAnchor) {
+        throw new Error('Cannot proceed without Anchor IDL support. Install cancelled by user.');
+    }
+
+    const rootNodeFromAnchor = await importModuleItem<(idl: unknown) => RootNode>(
+        'Anchor adapter',
+        '@codama/nodes-from-anchor',
+        'rootNodeFromAnchor',
+    );
+    return rootNodeFromAnchor(idl);
 }
 
 export function isRootNode(value: unknown): value is RootNode {

+ 10 - 9
packages/cli/src/utils/packageInstall.ts

@@ -16,18 +16,18 @@ export async function getPackageManagerInstallCommand(
     return createChildCommand(packageManager, args);
 }
 
-export async function installMissingDependencies(message: string, requiredDependencies: string[]): Promise<void> {
-    if (requiredDependencies.length === 0) return;
+export async function installMissingDependencies(message: string, requiredDependencies: string[]): Promise<boolean> {
+    if (requiredDependencies.length === 0) return true;
 
     const installedDependencies = await getPackageJsonDependencies({ includeDev: true });
     const missingDependencies = requiredDependencies.filter(dep => !installedDependencies.includes(dep));
-    if (missingDependencies.length === 0) return;
+    if (missingDependencies.length === 0) return true;
 
-    await installDependencies(message, missingDependencies);
+    return await installDependencies(message, missingDependencies);
 }
 
-export async function installDependencies(message: string, dependencies: string[]): Promise<void> {
-    if (dependencies.length === 0) return;
+export async function installDependencies(message: string, dependencies: string[]): Promise<boolean> {
+    if (dependencies.length === 0) return true;
     const installCommand = await getPackageManagerInstallCommand(dependencies);
 
     logWarning(message);
@@ -38,17 +38,18 @@ export async function installDependencies(message: string, dependencies: string[
         PROMPT_OPTIONS,
     );
     if (!dependencyResult.installDependencies) {
-        logWarning('Skipping installation. You can install manually later with:');
-        logWarning(pico.yellow(formatChildCommand(installCommand)));
-        return;
+        logWarning('Skipping installation.');
+        return false;
     }
 
     try {
         logInfo(`Installing`, dependencies);
         await spawnChildCommand(installCommand, { quiet: true });
         logSuccess(`Dependencies installed successfully.`);
+        return true;
     } catch {
         logError(`Failed to install dependencies. Please try manually:`);
         logError(pico.yellow(formatChildCommand(installCommand)));
+        return false;
     }
 }

+ 11 - 2
packages/cli/test/exports/mock-idl.json

@@ -1,6 +1,15 @@
 {
     "kind": "rootNode",
-    "spec": "codama",
+    "standard": "codama",
     "version": "1.0.0",
-    "program": { "kind": "programNode" }
+    "program": {
+        "kind": "programNode",
+        "name": "myProgram",
+        "accounts": [],
+        "instructions": [],
+        "definedTypes": [],
+        "errors": [],
+        "pdas": []
+    },
+    "additionalPrograms": []
 }

+ 0 - 3
pnpm-lock.yaml

@@ -83,9 +83,6 @@ importers:
       '@codama/nodes':
         specifier: workspace:*
         version: link:../nodes
-      '@codama/nodes-from-anchor':
-        specifier: workspace:*
-        version: link:../nodes-from-anchor
       '@codama/visitors':
         specifier: workspace:*
         version: link:../visitors