Skip to content

Commit a11fcdc

Browse files
authored
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.
1 parent d303f2a commit a11fcdc

File tree

8 files changed

+61
-19
lines changed

8 files changed

+61
-19
lines changed

.changeset/cyan-news-march.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@codama/cli': minor
3+
---
4+
5+
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.

packages/cli/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@
4444
},
4545
"dependencies": {
4646
"@codama/nodes": "workspace:*",
47-
"@codama/nodes-from-anchor": "workspace:*",
4847
"@codama/visitors": "workspace:*",
4948
"@codama/visitors-core": "workspace:*",
5049
"commander": "^14.0.0",

packages/cli/src/commands/init.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ import prompts, { PromptType } from 'prompts';
55
import { Config, ScriptConfig, ScriptName } from '../config';
66
import {
77
canRead,
8+
importModuleItem,
89
installMissingDependencies,
10+
isRootNode,
911
logBanner,
1012
logSuccess,
1113
PROMPT_OPTIONS,
@@ -120,7 +122,9 @@ async function getPromptResult(
120122
PROMPT_OPTIONS,
121123
);
122124

125+
const isAnchor = await isAnchorIdl(result.idlPath);
123126
await installMissingDependencies(`Your configuration requires additional dependencies.`, [
127+
...(isAnchor ? ['@codama/nodes-from-anchor'] : []),
124128
...(result.scripts.includes('js') ? ['@codama/renderers-js'] : []),
125129
...(result.scripts.includes('rust') ? ['@codama/renderers-rust'] : []),
126130
]);
@@ -194,3 +198,14 @@ function getContentForGill(result: PromptResult): string {
194198
`export default createCodamaConfig({\n${attributesString}});\n`
195199
);
196200
}
201+
202+
async function isAnchorIdl(idlPath: string): Promise<boolean> {
203+
const resolvedIdlPath = resolveRelativePath(idlPath);
204+
if (!(await canRead(resolvedIdlPath))) return false;
205+
try {
206+
const idlContent = await importModuleItem('IDL', resolvedIdlPath);
207+
return !isRootNode(idlContent);
208+
} catch {
209+
return false;
210+
}
211+
}

packages/cli/src/parsedConfig.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ async function parseConfig(
4545
): Promise<ParsedConfig> {
4646
const idlPath = parseIdlPath(config, configPath, options);
4747
const idlContent = await importModuleItem('IDL', idlPath);
48-
const rootNode = getRootNodeFromIdl(idlContent);
48+
const rootNode = await getRootNodeFromIdl(idlContent);
4949
const scripts = parseScripts(config.scripts ?? {}, configPath);
5050
const visitors = (config.before ?? []).map((v, i) => parseVisitorConfig(v, configPath, i, null));
5151

packages/cli/src/utils/nodes.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,30 @@
11
import type { RootNode } from '@codama/nodes';
2-
import { type AnchorIdl, rootNodeFromAnchor } from '@codama/nodes-from-anchor';
32

4-
export function getRootNodeFromIdl(idl: unknown): RootNode {
3+
import { importModuleItem } from './import';
4+
import { installMissingDependencies } from './packageInstall';
5+
6+
export async function getRootNodeFromIdl(idl: unknown): Promise<RootNode> {
57
if (typeof idl !== 'object' || idl === null) {
68
throw new Error('Unexpected IDL content. Expected an object, got ' + typeof idl);
79
}
810
if (isRootNode(idl)) {
911
return idl;
1012
}
11-
return rootNodeFromAnchor(idl as AnchorIdl);
13+
14+
const hasNodesFromAnchor = await installMissingDependencies(
15+
'Anchor IDL detected. Additional dependencies are required to process Anchor IDLs.',
16+
['@codama/nodes-from-anchor'],
17+
);
18+
if (!hasNodesFromAnchor) {
19+
throw new Error('Cannot proceed without Anchor IDL support. Install cancelled by user.');
20+
}
21+
22+
const rootNodeFromAnchor = await importModuleItem<(idl: unknown) => RootNode>(
23+
'Anchor adapter',
24+
'@codama/nodes-from-anchor',
25+
'rootNodeFromAnchor',
26+
);
27+
return rootNodeFromAnchor(idl);
1228
}
1329

1430
export function isRootNode(value: unknown): value is RootNode {

packages/cli/src/utils/packageInstall.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,18 @@ export async function getPackageManagerInstallCommand(
1616
return createChildCommand(packageManager, args);
1717
}
1818

19-
export async function installMissingDependencies(message: string, requiredDependencies: string[]): Promise<void> {
20-
if (requiredDependencies.length === 0) return;
19+
export async function installMissingDependencies(message: string, requiredDependencies: string[]): Promise<boolean> {
20+
if (requiredDependencies.length === 0) return true;
2121

2222
const installedDependencies = await getPackageJsonDependencies({ includeDev: true });
2323
const missingDependencies = requiredDependencies.filter(dep => !installedDependencies.includes(dep));
24-
if (missingDependencies.length === 0) return;
24+
if (missingDependencies.length === 0) return true;
2525

26-
await installDependencies(message, missingDependencies);
26+
return await installDependencies(message, missingDependencies);
2727
}
2828

29-
export async function installDependencies(message: string, dependencies: string[]): Promise<void> {
30-
if (dependencies.length === 0) return;
29+
export async function installDependencies(message: string, dependencies: string[]): Promise<boolean> {
30+
if (dependencies.length === 0) return true;
3131
const installCommand = await getPackageManagerInstallCommand(dependencies);
3232

3333
logWarning(message);
@@ -38,17 +38,18 @@ export async function installDependencies(message: string, dependencies: string[
3838
PROMPT_OPTIONS,
3939
);
4040
if (!dependencyResult.installDependencies) {
41-
logWarning('Skipping installation. You can install manually later with:');
42-
logWarning(pico.yellow(formatChildCommand(installCommand)));
43-
return;
41+
logWarning('Skipping installation.');
42+
return false;
4443
}
4544

4645
try {
4746
logInfo(`Installing`, dependencies);
4847
await spawnChildCommand(installCommand, { quiet: true });
4948
logSuccess(`Dependencies installed successfully.`);
49+
return true;
5050
} catch {
5151
logError(`Failed to install dependencies. Please try manually:`);
5252
logError(pico.yellow(formatChildCommand(installCommand)));
53+
return false;
5354
}
5455
}
Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
{
22
"kind": "rootNode",
3-
"spec": "codama",
3+
"standard": "codama",
44
"version": "1.0.0",
5-
"program": { "kind": "programNode" }
5+
"program": {
6+
"kind": "programNode",
7+
"name": "myProgram",
8+
"accounts": [],
9+
"instructions": [],
10+
"definedTypes": [],
11+
"errors": [],
12+
"pdas": []
13+
},
14+
"additionalPrograms": []
615
}

pnpm-lock.yaml

Lines changed: 0 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)