Skip to content

Commit

Permalink
ts: Lazy load workspace programs and improve program name accessor (c…
Browse files Browse the repository at this point in the history
  • Loading branch information
acheroncrypto committed Jul 25, 2023
1 parent 4604fbe commit 5eb678a
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 86 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- lang: Allow CPI calls matching an interface without pinning program ID ([#2559](https://github.com/coral-xyz/anchor/pull/2559)).
- cli, lang: Add IDL generation through compilation. `anchor build` still uses parsing method to generate IDLs, use `anchor idl build` to generate IDLs with the build method ([#2011](https://github.com/coral-xyz/anchor/pull/2011)).
- avm: Add support for the `.anchorversion` file to facilitate switching between different versions of the `anchor-cli` ([#2553](https://github.com/coral-xyz/anchor/pull/2553)).
- ts: Add ability to access workspace programs independent of the casing used, e.g. `anchor.workspace.myProgram`, `anchor.workspace.MyProgram`... ([#2579](https://github.com/coral-xyz/anchor/pull/2579)).

### Fixes

Expand All @@ -25,6 +26,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Support workspace inheritence ([#2570](https://github.com/coral-xyz/anchor/pull/2570)).
- client: Compile with Solana `1.14` ([#2572](https://github.com/coral-xyz/anchor/pull/2572)).
- cli: Fix `anchor build --no-docs` adding docs to the IDL ([#2575](https://github.com/coral-xyz/anchor/pull/2575)).
- ts: Load workspace programs on-demand rather than loading all of them at once ([#2579](https://github.com/coral-xyz/anchor/pull/2579)).

### Breaking

Expand Down
2 changes: 2 additions & 0 deletions tests/idl/Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ external = "Externa1111111111111111111111111111111111111"
generics = "Generics111111111111111111111111111111111111"
idl = "id11111111111111111111111111111111111111111"
relations_derivation = "Re1ationsDerivation111111111111111111111111"
non_existent = { address = "NonExistent11111111111111111111111111111111", idl = "non-existent.json" }
numbers_123 = { address = "Numbers111111111111111111111111111111111111", idl = "idls/relations_build_exp.json" }

[provider]
cluster = "localnet"
Expand Down
42 changes: 38 additions & 4 deletions tests/idl/tests/idl.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,43 @@
import * as anchor from "@coral-xyz/anchor";
import { assert } from "chai";

import { IDL } from "../target/types/idl";

describe(IDL.name, () => {
describe("IDL", () => {
anchor.setProvider(anchor.AnchorProvider.env());

it("Builds", () => {});
it("Can lazy load workspace programs", () => {
assert.doesNotThrow(() => {
// Program exists, should not throw
anchor.workspace.relationsDerivation;
});

assert.throws(() => {
// IDL path in Anchor.toml doesn't exist but other tests still run
// successfully because workspace programs are getting loaded on-demand
anchor.workspace.nonExistent;
}, /non-existent\.json/);
});

it("Can get workspace programs by their name independent of casing", () => {
const camel = anchor.workspace.relationsDerivation;
const pascal = anchor.workspace.RelationsDerivation;
const kebab = anchor.workspace["relations-derivation"];
const snake = anchor.workspace["relations_derivation"];

const compareProgramNames = (...programs: anchor.Program[]) => {
return programs.every(
(program) => program.idl.name === "relations_derivation"
);
};

assert(compareProgramNames(camel, pascal, kebab, snake));
});

it("Can use numbers in program names", () => {
assert.doesNotThrow(() => {
anchor.workspace.numbers123;
anchor.workspace.Numbers123;
anchor.workspace["numbers-123"];
anchor.workspace["numbers_123"];
});
});
});
142 changes: 60 additions & 82 deletions ts/packages/anchor/src/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,108 +1,86 @@
import camelCase from "camelcase";
import * as toml from "toml";
import { PublicKey } from "@solana/web3.js";
import { snakeCase } from "snake-case";
import { Program } from "./program/index.js";
import { Idl } from "./idl.js";
import { isBrowser } from "./utils/common.js";

let _populatedWorkspace = false;

/**
* The `workspace` namespace provides a convenience API to automatically
* search for and deserialize [[Program]] objects defined by compiled IDLs
* in an Anchor workspace.
*
* This API is for Node only.
*/
const workspace = new Proxy({} as any, {
get(workspaceCache: { [key: string]: Program }, programName: string) {
if (isBrowser) {
throw new Error("Workspaces aren't available in the browser");
}
const workspace = new Proxy(
{},
{
get(workspaceCache: { [key: string]: Program }, programName: string) {
if (isBrowser) {
throw new Error("Workspaces aren't available in the browser");
}

// Converting `programName` to snake_case enables the ability to use any
// of the following to access the workspace program:
// `workspace.myProgram`, `workspace.MyProgram`, `workspace["my-program"]`...
programName = snakeCase(programName);

// Check whether the program name contains any digits
if (/\d/.test(programName)) {
// Numbers cannot be properly converted from camelCase to snake_case,
// e.g. if the `programName` is `myProgram2`, the actual program name could
// be `my_program2` or `my_program_2`. This implementation assumes the
// latter as the default and always converts to `_numbers`.
//
// A solution to the conversion of program names with numbers in them
// would be to always convert the `programName` to camelCase instead of
// snake_case. The problem with this approach is that it would require
// converting everything else e.g. program names in Anchor.toml and IDL
// file names which are both snake_case.
programName = programName
.replace(/\d+/g, (match) => "_" + match)
.replace("__", "_");
}

const fs = require("fs");
const process = require("process");
// Return early if the program is in cache
if (workspaceCache[programName]) return workspaceCache[programName];

if (!_populatedWorkspace) {
const fs = require("fs");
const path = require("path");

let projectRoot = process.cwd();
while (!fs.existsSync(path.join(projectRoot, "Anchor.toml"))) {
const parentDir = path.dirname(projectRoot);
if (parentDir === projectRoot) {
projectRoot = undefined;
}
projectRoot = parentDir;
}
// Override the workspace programs if the user put them in the config.
const anchorToml = toml.parse(fs.readFileSync("Anchor.toml"));
const clusterId = anchorToml.provider.cluster;
const programEntry = anchorToml.programs?.[clusterId]?.[programName];

if (projectRoot === undefined) {
throw new Error("Could not find workspace root.");
let idlPath: string;
let programId;
if (typeof programEntry === "object" && programEntry.idl) {
idlPath = programEntry.idl;
programId = programEntry.address;
} else {
idlPath = path.join("target", "idl", `${programName}.json`);
}

const idlFolder = `${projectRoot}/target/idl`;
if (!fs.existsSync(idlFolder)) {
if (!fs.existsSync(idlPath)) {
throw new Error(
`${idlFolder} doesn't exist. Did you use "anchor build"?`
`${idlPath} doesn't exist. Did you run \`anchor build\`?`
);
}

const idlMap = new Map<string, Idl>();
fs.readdirSync(idlFolder)
.filter((file) => file.endsWith(".json"))
.forEach((file) => {
const filePath = `${idlFolder}/${file}`;
const idlStr = fs.readFileSync(filePath);
const idl = JSON.parse(idlStr);
idlMap.set(idl.name, idl);
const name = camelCase(idl.name, { pascalCase: true });
if (idl.metadata && idl.metadata.address) {
workspaceCache[name] = new Program(
idl,
new PublicKey(idl.metadata.address)
);
}
});

// Override the workspace programs if the user put them in the config.
const anchorToml = toml.parse(
fs.readFileSync(path.join(projectRoot, "Anchor.toml"), "utf-8")
);
const clusterId = anchorToml.provider.cluster;
if (anchorToml.programs && anchorToml.programs[clusterId]) {
attachWorkspaceOverride(
workspaceCache,
anchorToml.programs[clusterId],
idlMap
);
const idl = JSON.parse(fs.readFileSync(idlPath));
if (!programId) {
if (!idl.metadata?.address) {
throw new Error(
`IDL for program \`${programName}\` does not have \`metadata.address\` field.\n` +
"To add the missing field, run `anchor deploy` or `anchor test`."
);
}
programId = idl.metadata.address;
}
workspaceCache[programName] = new Program(idl, programId);

_populatedWorkspace = true;
}

return workspaceCache[programName];
},
});

function attachWorkspaceOverride(
workspaceCache: { [key: string]: Program },
overrideConfig: { [key: string]: string | { address: string; idl?: string } },
idlMap: Map<string, Idl>
) {
Object.keys(overrideConfig).forEach((programName) => {
const wsProgramName = camelCase(programName, { pascalCase: true });
const entry = overrideConfig[programName];
const overrideAddress = new PublicKey(
typeof entry === "string" ? entry : entry.address
);
let idl = idlMap.get(programName);
if (typeof entry !== "string" && entry.idl) {
idl = JSON.parse(require("fs").readFileSync(entry.idl, "utf-8"));
}
if (!idl) {
throw new Error(`Error loading workspace IDL for ${programName}`);
}
workspaceCache[wsProgramName] = new Program(idl, overrideAddress);
});
}
return workspaceCache[programName];
},
}
);

export default workspace;

0 comments on commit 5eb678a

Please sign in to comment.