@better-sol/cli 0.1.0-alpha.12 → 0.1.0-alpha.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3,11 +3,11 @@ import { createRequire } from "node:module";
3
3
  import { cancel, confirm, intro, isCancel, log, outro, select, spinner, text } from "@clack/prompts";
4
4
  import { Command } from "commander";
5
5
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
- import path, { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
6
+ import path, { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
7
7
  import { homedir, tmpdir } from "node:os";
8
8
  import { execFile, execSync } from "node:child_process";
9
9
  import { generateKeyPairSigner, getAddressDecoder } from "@solana/kit";
10
- import { access, mkdir, mkdtemp, opendir, readFile, rm, writeFile } from "node:fs/promises";
10
+ import { access, mkdir, mkdtemp, opendir, readFile, readdir, rm, writeFile } from "node:fs/promises";
11
11
  import { parseSync } from "oxc-parser";
12
12
  import { promisify } from "node:util";
13
13
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -3532,12 +3532,21 @@ function flattenAccountItems(items) {
3532
3532
  return result;
3533
3533
  }
3534
3534
  function idlTypeToCode(type) {
3535
- if (typeof type === "string") return `bs.${type}()`;
3536
- if ("option" in type && type.option !== void 0) return `bs.optional(${idlTypeToCode(type.option)})`;
3537
- if ("coption" in type && type.coption !== void 0) return `bs.optional(${idlTypeToCode(type.coption)})`;
3538
- if ("vec" in type && type.vec !== void 0) return `bs.vector(${idlTypeToCode(type.vec)})`;
3539
- if ("array" in type && type.array !== void 0) return `bs.array(${idlTypeToCode(type.array[0])}, ${type.array[1]})`;
3540
- if ("defined" in type) return "bs.pubkey()";
3535
+ if (typeof type === "string") {
3536
+ if (type === "publicKey") return "bs.pubkey()";
3537
+ return `bs.${type}()`;
3538
+ }
3539
+ if (typeof type !== "object" || type === null) return "bs.pubkey()";
3540
+ const record = type;
3541
+ const option = record.option;
3542
+ if (option !== void 0) return `bs.optional(${idlTypeToCode(option)})`;
3543
+ const coption = record.coption;
3544
+ if (coption !== void 0) return `bs.optional(${idlTypeToCode(coption)})`;
3545
+ const vec = record.vec;
3546
+ if (vec !== void 0) return `bs.vector(${idlTypeToCode(vec)})`;
3547
+ const array = record.array;
3548
+ if (Array.isArray(array) && array.length === 2) return `bs.array(${idlTypeToCode(array[0])}, ${array[1]})`;
3549
+ if (record.defined !== void 0) return "bs.pubkey()";
3541
3550
  return "bs.pubkey()";
3542
3551
  }
3543
3552
  function escapeString(str) {
@@ -3553,18 +3562,35 @@ function getStructFields(typeDef) {
3553
3562
  if (!Array.isArray(fields)) return [];
3554
3563
  return fields;
3555
3564
  }
3565
+ function getStructFieldsFromAccount(account) {
3566
+ if (typeof account !== "object" || account === null) return [];
3567
+ const accountType = account.type;
3568
+ if (typeof accountType !== "object" || accountType === null) return [];
3569
+ const typeRecord = accountType;
3570
+ if (typeRecord.kind !== "struct") return [];
3571
+ const fields = typeRecord.fields;
3572
+ if (!Array.isArray(fields)) return [];
3573
+ return fields;
3574
+ }
3575
+ function getIdlName$1(idl) {
3576
+ if (typeof idl.metadata?.name === "string") return idl.metadata.name;
3577
+ const record = idl;
3578
+ return typeof record.name === "string" ? record.name : void 0;
3579
+ }
3556
3580
  function generateIdlProgram(idl, sourceLabel) {
3557
3581
  const w = new CodeWriter();
3558
- const programName = idl.metadata?.name ?? "unknown";
3582
+ const programName = toCamel(getIdlName$1(idl) ?? "unknown");
3559
3583
  const programAddress = idl.address ?? "";
3560
3584
  const typesByName = /* @__PURE__ */ new Map();
3561
3585
  for (const t of idl.types ?? []) typesByName.set(t.name, t);
3562
3586
  const accountDefs = idl.accounts ?? [];
3563
3587
  const resolvedAccounts = /* @__PURE__ */ new Map();
3564
3588
  for (const acc of accountDefs) {
3565
- const fields = getStructFields(typesByName.get(acc.name));
3589
+ const fromTypes = getStructFields(typesByName.get(acc.name));
3590
+ const fromAccount = getStructFieldsFromAccount(acc);
3591
+ const fields = fromTypes.length > 0 ? fromTypes : fromAccount;
3566
3592
  resolvedAccounts.set(acc.name, {
3567
- name: acc.name,
3593
+ exportName: toPascal(acc.name),
3568
3594
  fields
3569
3595
  });
3570
3596
  }
@@ -3573,11 +3599,11 @@ function generateIdlProgram(idl, sourceLabel) {
3573
3599
  w.line(`// Generated by better-sol from ${sourceLabel}. Do not edit.`);
3574
3600
  w.line(`import { bs } from "better-sol/program"`);
3575
3601
  w.blank();
3576
- for (const [name, { fields }] of resolvedAccounts) {
3602
+ for (const [, { exportName, fields }] of resolvedAccounts) {
3577
3603
  if (fields.length === 0) continue;
3578
- w.line(`const ${name} = bs.account({`);
3604
+ w.line(`const ${exportName} = bs.account({`);
3579
3605
  w.indent(1);
3580
- for (const field of fields) w.line(`${field.name}: ${idlTypeToCode(field.type)},`);
3606
+ for (const field of fields) w.line(`${toCamel(field.name)}: ${idlTypeToCode(field.type)},`);
3581
3607
  w.indent(0);
3582
3608
  w.line("})");
3583
3609
  w.blank();
@@ -3590,10 +3616,9 @@ function generateIdlProgram(idl, sourceLabel) {
3590
3616
  w.line(`address: "${escapeString(programAddress)}",`);
3591
3617
  if (resolvedAccounts.size > 0) {
3592
3618
  w.line(`accounts: {`);
3593
- for (const name of resolvedAccounts.keys()) {
3594
- const resolved = resolvedAccounts.get(name);
3595
- if (resolved === void 0 || resolved.fields.length === 0) continue;
3596
- w.line(` ${name},`);
3619
+ for (const [, { exportName, fields }] of resolvedAccounts) {
3620
+ if (fields.length === 0) continue;
3621
+ w.line(` ${exportName},`);
3597
3622
  }
3598
3623
  w.line(`},`);
3599
3624
  }
@@ -3601,7 +3626,7 @@ function generateIdlProgram(idl, sourceLabel) {
3601
3626
  w.line(`errors: {`);
3602
3627
  for (const err of errorEntries) {
3603
3628
  const msg = err.msg ?? err.name;
3604
- w.line(` ${err.name}: "${escapeString(msg)}",`);
3629
+ w.line(` ${toPascal(err.name)}: "${escapeString(msg)}",`);
3605
3630
  }
3606
3631
  w.line(`},`);
3607
3632
  }
@@ -3609,8 +3634,8 @@ function generateIdlProgram(idl, sourceLabel) {
3609
3634
  w.line(`events: {`);
3610
3635
  for (const ev of eventEntries) {
3611
3636
  const fields = getStructFields(typesByName.get(ev.name));
3612
- w.line(` ${ev.name}: {`);
3613
- for (const f of fields) w.line(` ${f.name}: ${idlTypeToCode(f.type)},`);
3637
+ w.line(` ${toPascal(ev.name)}: {`);
3638
+ for (const f of fields) w.line(` ${toCamel(f.name)}: ${idlTypeToCode(f.type)},`);
3614
3639
  w.line(` },`);
3615
3640
  }
3616
3641
  w.line(`},`);
@@ -3619,21 +3644,22 @@ function generateIdlProgram(idl, sourceLabel) {
3619
3644
  w.line(`}, ix => ({`);
3620
3645
  for (const instr of idl.instructions) {
3621
3646
  const flatAccounts = flattenAccountItems(instr.accounts);
3647
+ const ixName = toCamel(instr.name);
3622
3648
  w.blank();
3623
- w.line(`${instr.name}: ix({`);
3649
+ w.line(`${ixName}: ix({`);
3624
3650
  if (flatAccounts.length > 0) {
3625
3651
  w.line(` accounts: {`);
3626
3652
  for (const acc of flatAccounts) {
3627
3653
  if (acc.optional === true) continue;
3628
3654
  const constraint = accountToConstraint(acc, resolvedAccounts);
3629
- w.line(` ${acc.name}: ${constraint},`);
3655
+ w.line(` ${toCamel(acc.name)}: ${constraint},`);
3630
3656
  }
3631
3657
  w.line(` },`);
3632
3658
  }
3633
3659
  const args = instr.args ?? [];
3634
3660
  if (args.length > 0) {
3635
3661
  w.line(` args: {`);
3636
- for (const arg of args) w.line(` ${arg.name}: ${idlTypeToCode(arg.type)},`);
3662
+ for (const arg of args) w.line(` ${toCamel(arg.name)}: ${idlTypeToCode(arg.type)},`);
3637
3663
  w.line(` },`);
3638
3664
  }
3639
3665
  if (instr.returns !== void 0) w.line(` returns: ${idlTypeToCode(instr.returns)},`);
@@ -3649,15 +3675,22 @@ function generateIdlProgram(idl, sourceLabel) {
3649
3675
  }
3650
3676
  function accountToConstraint(acc, resolvedAccounts) {
3651
3677
  if (acc.signer === true) return "bs.signer()";
3652
- const accountRef = resolvedAccounts.has(acc.name) ? acc.name : "bs.account({})";
3678
+ const resolved = resolveAccountDefinition(acc.name, resolvedAccounts);
3679
+ const accountRef = resolved !== void 0 && resolved.fields.length > 0 ? resolved.exportName : "bs.account({})";
3653
3680
  if (acc.writable === true) return `bs.mut(${accountRef})`;
3654
3681
  return accountRef;
3655
3682
  }
3683
+ function resolveAccountDefinition(accountName, resolvedAccounts) {
3684
+ const exact = resolvedAccounts.get(accountName);
3685
+ if (exact !== void 0) return exact;
3686
+ const pascalName = toPascal(accountName);
3687
+ for (const [idlName, account] of resolvedAccounts) if (idlName === pascalName || account.exportName === pascalName) return account;
3688
+ }
3656
3689
  function runParams(accounts, args) {
3657
3690
  const parts = [];
3658
- const accountNames = accounts.filter((a) => a.optional !== true).map((a) => a.name);
3691
+ const accountNames = accounts.filter((a) => a.optional !== true).map((a) => toCamel(a.name));
3659
3692
  if (accountNames.length > 0) parts.push(`{ ${accountNames.join(", ")} }`);
3660
- const argNames = args.map((a) => a.name);
3693
+ const argNames = args.map((a) => toCamel(a.name));
3661
3694
  if (argNames.length > 0) parts.push(`{ ${argNames.join(", ")} }`);
3662
3695
  parts.push("ctx");
3663
3696
  return parts.join(", ");
@@ -69245,7 +69278,7 @@ async function generateIdl(input, options) {
69245
69278
  sourceLabel = basename(idlPath);
69246
69279
  s.stop("IDL loaded");
69247
69280
  }
69248
- const programName = options.name ?? idl.metadata?.name ?? "program";
69281
+ const programName = toCamel(options.name ?? getIdlName(idl) ?? "program");
69249
69282
  const outPath = cwdPath(options.out ?? `generated/${programName}.ts`);
69250
69283
  s.message("Generating program definition");
69251
69284
  const code = generateIdlProgram(idl, sourceLabel);
@@ -69253,12 +69286,101 @@ async function generateIdl(input, options) {
69253
69286
  await writeFile(outPath, code, "utf-8");
69254
69287
  s.stop("Generated");
69255
69288
  log.info(`Wrote ${resolve(outPath)}`);
69256
- const importPath = removeExt(relative(dirname(outPath), outPath));
69257
- log.step(`Import it in your app:\n\n import { ${programName} } from "./${importPath}"\n\nThen register it with the client:\n\n const sol = await betterSol({ programs: { ${programName} } })`);
69289
+ log.step(await buildImportGuide(outPath, programName));
69258
69290
  outro("Done");
69259
69291
  }
69292
+ const IGNORED_DIRECTORIES = new Set([
69293
+ ".better-sol",
69294
+ ".git",
69295
+ ".next",
69296
+ "dist",
69297
+ "build",
69298
+ "node_modules"
69299
+ ]);
69300
+ async function buildImportGuide(outPath, programName) {
69301
+ const usage = await findBetterSolUsage(process.cwd());
69302
+ if (usage !== void 0) return `Import it in ${relativePath(process.cwd(), usage.filePath)}:\n\n import { ${programName} } from "${moduleSpecifier(dirname(usage.filePath), outPath)}"\n\n${usage.hasProgramsObject ? `Add it to the existing programs object:\n\n programs: { ..., ${programName} }` : `Register it with the client:\n\n const sol = await betterSol({ programs: { ${programName} } })`}`;
69303
+ return `Import it in your app:\n\n import { ${programName} } from "${moduleSpecifier(process.cwd(), outPath)}"\n\nThen register it with the client:\n\n const sol = await betterSol({ programs: { ${programName} } })`;
69304
+ }
69305
+ async function findBetterSolUsage(root) {
69306
+ const files = await collectTypeScriptFiles(root);
69307
+ let fallback;
69308
+ for (const filePath of files) {
69309
+ const usage = parseBetterSolUsage(await readFile(filePath, "utf-8"), filePath);
69310
+ if (usage === void 0) continue;
69311
+ if (usage.hasProgramsObject) return usage;
69312
+ fallback ??= usage;
69313
+ }
69314
+ return fallback;
69315
+ }
69316
+ async function collectTypeScriptFiles(root) {
69317
+ const results = [];
69318
+ await collectTypeScriptFilesInto(root, results);
69319
+ return results.sort();
69320
+ }
69321
+ async function collectTypeScriptFilesInto(directory, results) {
69322
+ const entries = await readdir(directory, { withFileTypes: true });
69323
+ for (const entry of entries) {
69324
+ const fullPath = join(directory, entry.name);
69325
+ if (entry.isDirectory()) {
69326
+ if (!IGNORED_DIRECTORIES.has(entry.name)) await collectTypeScriptFilesInto(fullPath, results);
69327
+ continue;
69328
+ }
69329
+ if (!entry.isFile()) continue;
69330
+ const extension = extname(entry.name);
69331
+ if (extension === ".ts" || extension === ".tsx") results.push(fullPath);
69332
+ }
69333
+ }
69334
+ function parseBetterSolUsage(source, filePath) {
69335
+ let program;
69336
+ try {
69337
+ program = parseModule(filePath, source);
69338
+ } catch {
69339
+ return;
69340
+ }
69341
+ let hasCall = false;
69342
+ let hasProgramsObject = false;
69343
+ visitAst(program, (node) => {
69344
+ if (!isBetterSolCall(node)) return;
69345
+ hasCall = true;
69346
+ const firstArg = node.arguments[0];
69347
+ if (!isObjectExpression(firstArg)) return;
69348
+ if (isObjectExpression(getObjectProperty(firstArg, "programs"))) hasProgramsObject = true;
69349
+ });
69350
+ return hasCall ? {
69351
+ filePath,
69352
+ hasProgramsObject
69353
+ } : void 0;
69354
+ }
69355
+ function isBetterSolCall(node) {
69356
+ return isCallExpression(node) && isIdentifier(node.callee) && node.callee.name === "betterSol";
69357
+ }
69358
+ function visitAst(node, visit) {
69359
+ if (!isAstNode(node)) return;
69360
+ visit(node);
69361
+ for (const value of Object.values(node)) if (Array.isArray(value)) for (const item of value) visitAst(item, visit);
69362
+ else visitAst(value, visit);
69363
+ }
69364
+ function isAstNode(value) {
69365
+ if (typeof value !== "object" || value === null) return false;
69366
+ return typeof value.type === "string";
69367
+ }
69368
+ function moduleSpecifier(fromDirectory, toFile) {
69369
+ const normalized = removeExt(relative(fromDirectory, toFile)).split(sep).join("/");
69370
+ if (normalized.startsWith(".")) return normalized;
69371
+ return `./${normalized}`;
69372
+ }
69373
+ function relativePath(fromDirectory, toFile) {
69374
+ const value = relative(fromDirectory, toFile).split(sep).join("/");
69375
+ return value.length > 0 ? value : ".";
69376
+ }
69260
69377
  function removeExt(path) {
69261
- return path.replace(/\.ts$/, "");
69378
+ return path.replace(/\.tsx?$/, "");
69379
+ }
69380
+ function getIdlName(idl) {
69381
+ if (typeof idl.metadata?.name === "string") return idl.metadata.name;
69382
+ const record = idl;
69383
+ return typeof record.name === "string" ? record.name : void 0;
69262
69384
  }
69263
69385
  //#endregion
69264
69386
  //#region src/commands/login.ts
@@ -69360,7 +69482,7 @@ async function gitCommit() {
69360
69482
  //#endregion
69361
69483
  //#region src/index.ts
69362
69484
  const cli = new Command();
69363
- cli.name("better-sol").description("Write Solana programs in TypeScript. Run with npx @better-sol/cli@alpha").version("0.1.0-alpha.12");
69485
+ cli.name("better-sol").description("Write Solana programs in TypeScript. Run with npx @better-sol/cli@alpha").version("0.1.0-alpha.13");
69364
69486
  cli.command("init").description("Initialize a better-sol project").option("--force", "overwrite existing files", false).option("--skip-install", "skip installing dependencies", false).action((options) => run(() => init(options)));
69365
69487
  cli.command("create").description("Create a new better-sol program").argument("[name]", "program name").option("--dir <dir>", "program directory", "programs").option("--force", "overwrite existing files", false).action((name, options) => run(() => create(name, options)));
69366
69488
  cli.command("login").description("Save your compiler API key").argument("[apiKey]", "compiler API key").action((apiKey) => run(() => login(apiKey)));