-
Notifications
You must be signed in to change notification settings - Fork 1.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ts: Lazy load workspace programs and improve program name accessor #2579
Merged
acheroncrypto
merged 6 commits into
coral-xyz:master
from
acheroncrypto:ts-workspace-lazy-load-and-casing
Jul 25, 2023
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
0c796ac
Lazy load workspace programs and improve program name accessor
acheroncrypto f1ddc1e
Always check the IDL path to show a better error message
acheroncrypto ad55e7e
Fix snake cased numbers in program names
acheroncrypto 5e7165c
Add tests
acheroncrypto 4e1107b
Update CHANGELOG
acheroncrypto 07ffabd
Fix incorrect PR number in CHANGELOG
acheroncrypto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"]; | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We used
my_program2
and this is a pretty big breaking change since there's no way to override the name. We're now stuck having to rename our program everywhereThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also prefer
my_program2
instead ofmy_program_2
but we had examples using the other way:anchor/examples/tutorial/basic-2/programs/basic-2/Cargo.toml
Line 10 in d9a9f19
This was a quick solution to fix CI errors but I think we should either support both ways or provide a good error message that explains how to fix the name related issues.