-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(all): Misti 0.4 support + Interactive project selection
Closes #6
- Loading branch information
Showing
7 changed files
with
364 additions
and
104 deletions.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
## Building and testing locally | ||
|
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 |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** | ||
* Various utilities to work with Blueprint internals and its generated files. | ||
* | ||
* @packageDocumentation | ||
*/ | ||
|
||
import { | ||
createSourceFile, | ||
ScriptTarget, | ||
Node, | ||
isVariableDeclaration, | ||
ObjectLiteralExpression, | ||
isPropertyAssignment, | ||
StringLiteral, | ||
Expression, | ||
forEachChild, | ||
} from "typescript"; | ||
import { Args } from "@ton/blueprint"; | ||
import { promises as fs } from "fs"; | ||
import path from "path"; | ||
|
||
/** | ||
* Tact project info parsed from the Blueprint compilation wrapper. | ||
*/ | ||
export type TactProjectInfo = { | ||
projectName: string; | ||
target: string; | ||
options: Record<string, unknown>; | ||
}; | ||
|
||
/** | ||
* Blueprint generates TypeScript wrappers that define compilation options in | ||
* the following format: | ||
* | ||
* ```typescript | ||
* import { CompilerConfig } from '@ton/blueprint'; | ||
* export const compile: CompilerConfig = { | ||
* lang: 'tact', | ||
* target: 'contracts/test1.tact', | ||
* options: { | ||
* debug: true, | ||
* }, | ||
* }; | ||
* ``` | ||
* | ||
* This function extracts the `target` and `options` values parsing the wrapper file. | ||
*/ | ||
async function parseCompileWrapper( | ||
filePath: string, | ||
): Promise<TactProjectInfo | undefined> { | ||
const projectName = path.basename(filePath).replace(".compile.ts", ""); | ||
const content = await fs.readFile(filePath, "utf-8"); | ||
const sourceFile = createSourceFile( | ||
filePath, | ||
content, | ||
ScriptTarget.ESNext, | ||
true, | ||
); | ||
let target: string | undefined; | ||
let options: Record<string, unknown> = {} as Record<string, unknown>; | ||
function findNodes(node: Node) { | ||
if (isVariableDeclaration(node) && node.name.getText() === "compile") { | ||
const initializer = node.initializer as ObjectLiteralExpression; | ||
for (const property of initializer.properties) { | ||
if (isPropertyAssignment(property)) { | ||
if (property.name.getText() === "target") { | ||
target = (property.initializer as StringLiteral).text; | ||
} | ||
if (property.name.getText() === "options") { | ||
const optionsObj: Record<string, unknown> = {}; | ||
( | ||
property.initializer as ObjectLiteralExpression | ||
).properties.forEach((prop) => { | ||
if (isPropertyAssignment(prop)) { | ||
optionsObj[prop.name.getText()] = ( | ||
prop.initializer as Expression | ||
).getText(); | ||
} | ||
}); | ||
options = optionsObj; | ||
} | ||
} | ||
} | ||
} | ||
forEachChild(node, findNodes); | ||
} | ||
findNodes(sourceFile); | ||
return target ? { projectName, target, options } : undefined; | ||
} | ||
|
||
/** | ||
* Extracts an information from the TypeScript wrapper file genreated by Blueprint. | ||
*/ | ||
export async function extractProjectInfo( | ||
blueprintCompilePath: string, | ||
): Promise<TactProjectInfo | undefined> { | ||
const filePath = path.resolve(__dirname, blueprintCompilePath); | ||
console.log('Parsing', filePath); | ||
return parseCompileWrapper(filePath); | ||
} | ||
|
||
/** | ||
* Converts Blueprint arguments to a list of strings. | ||
*/ | ||
export function argsToStringList(args: Args): string[] { | ||
const argsList: string[] = args._; | ||
Object.entries(args).forEach(([key, value]) => { | ||
if (key !== "_" && value !== undefined) { | ||
if (typeof value === "boolean") { | ||
argsList.push(key); | ||
} else { | ||
argsList.push(key, value.toString()); | ||
} | ||
} | ||
}); | ||
return argsList; | ||
} |
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 |
---|---|---|
@@ -0,0 +1,135 @@ | ||
import { Args, UIProvider } from "@ton/blueprint"; | ||
import { findCompiles, selectFile } from "@ton/blueprint/dist/utils"; | ||
import { | ||
TactProjectInfo, | ||
extractProjectInfo, | ||
argsToStringList, | ||
} from "./blueprint"; | ||
import { | ||
MistiResult, | ||
runMistiCommand, | ||
createMistiCommand, | ||
} from "@nowarp/misti/dist/cli"; | ||
import { setStdlibPath } from "./stdlibPaths"; | ||
import fs from "fs"; | ||
import path from "path"; | ||
import os from "os"; | ||
|
||
/** | ||
* Interactively selects one of the Tact projects available in the Blueprint compile wrapper. | ||
*/ | ||
async function selectProject( | ||
ui: UIProvider, | ||
args: Args, | ||
): Promise<TactProjectInfo | undefined> { | ||
const result = await selectFile(await findCompiles(), { | ||
ui, | ||
hint: args._.length > 1 && args._[1].length > 0 ? args._[1] : undefined, | ||
import: false, | ||
}); | ||
if (!fs.existsSync(result.path)) { | ||
ui.write( | ||
[ | ||
`❌ Cannot access ${result.path}`, | ||
"Please specify path to your contract directly: `yarn blueprint misti path/to/contract.tact`", | ||
].join("\n"), | ||
); | ||
return undefined; | ||
} | ||
const projectInfo = await extractProjectInfo(result.path); | ||
if (projectInfo === undefined) { | ||
ui.write( | ||
[ | ||
`❌ Cannot extract project information from ${result.path}`, | ||
"Please specify path to your contract directly: `yarn blueprint misti path/to/contract.tact`", | ||
].join("\n"), | ||
); | ||
return undefined; | ||
} | ||
return projectInfo; | ||
} | ||
|
||
export class MistiExecutor { | ||
private constructor( | ||
private projectName: string, | ||
private args: string[], | ||
private ui: UIProvider, | ||
) {} | ||
public static async fromArgs( | ||
args: Args, | ||
ui: UIProvider, | ||
): Promise<MistiExecutor | undefined> { | ||
let argsStr = argsToStringList(args).slice(1); | ||
const command = createMistiCommand(); | ||
|
||
let tactPathIsDefined = true; | ||
const originalArgsStr = [...argsStr]; | ||
try { | ||
await command.parseAsync(argsStr, { from: "user" }); | ||
} catch (error) { | ||
tactPathIsDefined = false; | ||
if (error instanceof Error && error.message.includes('is required')) { | ||
const tempPath = '/tmp/contract.tact'; | ||
argsStr.push(tempPath); | ||
await command.parseAsync(argsStr, { from: "user" }); | ||
} else { | ||
throw error; | ||
} | ||
} | ||
argsStr = originalArgsStr; | ||
|
||
if (tactPathIsDefined) { | ||
// The path to the Tact configuration or contract is explicitly specified | ||
// in arguments (e.g. yarn blueprint misti path/to/contract.tact). | ||
const tactPath = command.args[0]; | ||
const projectName = path.basename(tactPath).split(".")[0]; | ||
return new MistiExecutor(projectName, argsStr, ui); | ||
} | ||
|
||
// Interactively select the project | ||
const project = await selectProject(ui, args); | ||
if (!project) return undefined; | ||
try { | ||
const tactPath = this.generateTactConfig(project, "."); | ||
argsStr.push(tactPath); | ||
return new MistiExecutor(project.projectName, argsStr, ui); | ||
} catch { | ||
ui.write("❌ Cannot create a Tact config in current directory"); | ||
return undefined; | ||
} | ||
} | ||
|
||
/** | ||
* Generates the Tact configuration file based on the Blueprint compilation output. | ||
* | ||
* @param outDir Directory to save the generated file | ||
* @throws If it is not possible to create a path | ||
* @returns Absolute path to the generated config | ||
*/ | ||
private static generateTactConfig( | ||
config: TactProjectInfo, | ||
outDir: string, | ||
): string | never { | ||
const content = JSON.stringify({ | ||
projects: [ | ||
{ | ||
name: config.projectName, | ||
path: config.target, | ||
output: path.join(os.tmpdir(), "tact-output"), | ||
options: config.options, | ||
}, | ||
], | ||
}); | ||
const outPath = path.join(outDir, "tact.config.json"); | ||
fs.writeFileSync(outPath, content); | ||
return outPath; | ||
} | ||
|
||
public async execute(): Promise<MistiResult> { | ||
this.ui.write(`⏳ Checking ${this.projectName}...\n`); | ||
setStdlibPath(this.args); | ||
// ! is safe: it could not be undefined in Misti 0.4+ | ||
const result = (await runMistiCommand(this.args))!; | ||
return result[1]; | ||
} | ||
} |
Oops, something went wrong.