Skip to content

feat: allow a special CLI argument --quick-start and add a special template with counter and open-ended poll #9

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
96 changes: 57 additions & 39 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { mkdir, readdir, readFile, stat, writeFile } from "fs/promises";
import { dirname, join, relative } from "path";
import { createInterface, Interface } from "readline/promises";
import { stdin, stdout } from 'process';
import { stdin, stdout } from "process";
import { exec } from "child_process";
import { promisify } from "util";

Expand All @@ -24,20 +24,20 @@ async function getFiles(dir: string): Promise<string[]> {
}

function kebabToPascal(input: string): string {
return ('-' + input).replace(/-(\w)/g, (_, c) => c.toUpperCase());
return ("-" + input).replace(/-(\w)/g, (_, c) => c.toUpperCase());
}

async function reprompt<T>(cb: () => Promise<T | undefined>): Promise<T> {
for (; ;) {
const result = await cb();
if (typeof result !== 'undefined') {
if (typeof result !== "undefined") {
return result;
}
}
}

export const runCommand = (command: string, cwd: string = process.cwd()) => {
const env = { ...process.env, FORCE_COLOR: process.stdout.isTTY ? '1' : '0' };
const env = { ...process.env, FORCE_COLOR: process.stdout.isTTY ? "1" : "0" };
const thread = exec(command, { cwd, env });
return new Promise<boolean>((resolve, reject) => {
thread.stdout?.pipe(process.stdout);
Expand Down Expand Up @@ -67,7 +67,7 @@ async function folderExists(path: string): Promise<boolean> {
await stat(path);
return true;
} catch (err: any) {
if (err.code === 'ENOENT') {
if (err.code === "ENOENT") {
return false;
}
throw err;
Expand All @@ -78,41 +78,52 @@ const execAsync = promisify(exec);

export async function isGitInstalled(): Promise<boolean> {
try {
await execAsync('git --version');
await execAsync("git --version");
return true;
} catch {
return false;
}
}

function detectPackageManager() {
const pkgManager = process.env.npm_config_user_agent?.split(' ')[0]?.split('/')[0] ?? 'npm';
const pkgManager = process.env.npm_config_user_agent?.split(" ")[0]?.split("/")[0] ?? "npm";

switch (pkgManager) {
case 'yarn': return { name: 'yarn', install: 'yarn', run: 'yarn' } as const;
case 'pnpm': return { name: 'pnpm', install: 'pnpm install', run: 'pnpm run' } as const;
case 'bun': return { name: 'bun', install: 'bun install', run: 'bun run' } as const;
default: return { name: 'npm', install: 'npm install', run: 'npm run' } as const;
case "yarn": return { name: "yarn", install: "yarn", run: "yarn" } as const;
case "pnpm": return { name: "pnpm", install: "pnpm install", run: "pnpm run" } as const;
case "bun": return { name: "bun", install: "bun install", run: "bun run" } as const;
default: return { name: "npm", install: "npm install", run: "npm run" } as const;
}
}

const reservedTactWords = [
"Int", "Bool", "Builder", "Slice", "Cell", "Address", "String", "StringBuilder",
];

const templateNameRoots = {
"Empty contract": join(__dirname, "../template/empty"),
"Quick start": join(__dirname, "../template/quick-start"),
};

async function main(reader: Interface) {
const templateRoot = join(__dirname, "../template/empty");

const packageName = await reprompt(async () => {
const placeholder = 'example';
const fullPrompt = `Enter package name (${placeholder}): `;
const result = (await reader.question(fullPrompt)) || placeholder;
const validPackageName = /^(?=.{1,214}$)(?:@[a-z0-9]+(?:[._-][a-z0-9]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
if (!result.match(validPackageName)) {
return;
}
return result;
});
let templateRoot = templateNameRoots["Quick start"];
let packageName = "tact-quick-start";
let contractName = "QuickStart";
let isQuickStart = process.argv.slice(2).includes("--quick-start");

if (!isQuickStart) {
templateRoot = templateNameRoots["Empty contract"];
packageName = await reprompt(async () => {
const placeholder = "example";
const fullPrompt = `Enter package name (${placeholder}): `;
const result = (await reader.question(fullPrompt)) || placeholder;
const validPackageName = /^(?=.{1,214}$)(?:@[a-z0-9]+(?:[._-][a-z0-9]+)*\/)?[a-z0-9]+(?:[._-][a-z0-9]+)*$/;
if (!result.match(validPackageName)) {
return;
}
return result;
});
}

const targetRoot = join(process.cwd(), packageName);

Expand All @@ -137,16 +148,18 @@ async function main(reader: Interface) {
// });
}

const contractName = await reprompt(async () => {
const placeholder = kebabToPascal(packageName);
const fullPrompt = `Enter contract name (${placeholder}): `;
const result = (await reader.question(fullPrompt)) || placeholder;
const validContractName = /[A-Z][a-zA-Z0-9_]*/;
if (!result.match(validContractName) || reservedTactWords.includes(result)) {
return;
}
return result;
});
if (!isQuickStart) {
contractName = await reprompt(async () => {
const placeholder = kebabToPascal(packageName);
const fullPrompt = `Enter contract name (${placeholder}): `;
const result = (await reader.question(fullPrompt)) || placeholder;
const validContractName = /[A-Z][a-zA-Z0-9_]*/;
if (!result.match(validContractName) || reservedTactWords.includes(result)) {
return;
}
return result;
});
}

const manager = detectPackageManager();

Expand Down Expand Up @@ -194,21 +207,26 @@ async function main(reader: Interface) {
process.exit(31);
}
} else {
console.error('Git is not installed');
console.error('Git repository will not be initialized');
console.error("Git is not installed");
console.error("Git repository will not be initialized");
}

console.log('To switch to generated project, use');
console.log(`cd ${relative(process.cwd(), targetRoot)}`)
const relDir = relative(process.cwd(), targetRoot);
console.log("\nTo switch to the generated project, use\n");
console.error(` cd ${relDir}`);

if (isQuickStart) {
console.log(`\nThen see the ${relDir}/README.md to get started!`);
}
}

async function withReader<T>(cb: (reader: Interface) => Promise<T>): Promise<T> {
const reader = createInterface({ input: stdin, output: stdout });
try {
return await cb(reader);
} finally {
reader.close()
reader.close();
}
}

void withReader(main);
void withReader(main);
50 changes: 50 additions & 0 deletions template/quick-start/.github/workflows/pull.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: CI

on:
push:
branches: ["main"]
pull_request:
branches: ["**"]
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
test:
strategy:
fail-fast: false
matrix:
node-version: [22]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}

- name: Enable corepack
run: corepack enable

- name: Activate chosen package manager
run: corepack prepare --activate

- name: Install dependencies
run: pmInstall

- name: Build
run: pmRun build

- name: Run Tact formatter
run: pmRun fmt:check

- name: Run Misti
run: pmRun lint

- name: Run tests
run: pmRun test
5 changes: 5 additions & 0 deletions template/quick-start/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 4,
"useTabs": false
}
3 changes: 3 additions & 0 deletions template/quick-start/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"recommendations": ["tonstudio.vscode-tact", "tonwhales.func-vscode", "dbaeumer.vscode-eslint"]
}
95 changes: 95 additions & 0 deletions template/quick-start/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Tact Quick Start

Tact is a fresh programming language for TON Blockchain, focused on efficiency and ease of development. It is a good fit for complex smart contracts, quick onboarding and rapid prototyping.

This quick start guide will help you [set up your editor or IDE](#editor-setup), understand some of the basics of Tact and TON, and learn from the given Tact smart contracts: a counter and an open-ended poll.

We'll also cover how to create, test, and deploy contracts using the provided template, which includes a complete development environment with all the necessary tools pre-configured.

## Editor setup

- [VS Code extension](https://marketplace.visualstudio.com/items?itemName=tonstudio.vscode-tact) - Powerful and feature-rich extension for Visual Studio Code (VSCode) and VSCode-based editors like VSCodium, Cursor, Windsurf, and others.
- Get it on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=tonstudio.vscode-tact).
- Get it on the [Open VSX Registry](https://open-vsx.org/extension/tonstudio/vscode-tact).
- Or install from the [`.vsix` files in nightly releases](https://github.com/tact-lang/tact-language-server/releases).
- [Language Server (LSP Server)](https://github.com/tact-lang/tact-language-server) - Supports Sublime Text, (Neo)Vim, Helix, and other editors with LSP support.
- [Intelli Tact](https://plugins.jetbrains.com/plugin/27290-tact) - Powerful and feature-rich plugin for JetBrains IDEs like WebStorm, IntelliJ IDEA, and others.
- [tact.vim](https://github.com/tact-lang/tact.vim) - Vim 8+ plugin.
- [tact-sublime](https://github.com/tact-lang/tact-sublime) - Sublime Text 4 package.
- Get it on the [Package Control](https://packagecontrol.io/packages/Tact).
- [tree-sitter-tact](https://github.com/tact-lang/tree-sitter-tact) - Tree-sitter grammar for Tact.

The language server, VSCode extension, and JetBrains plugin linked above come with hover documentation for built-in keywords, which is very helpful for discovering the language on your own!

If you're done with the setup, let's discuss the contents of this project.

## Project structure

Start exploring this project with a Counter contract. Once ready, move to the Poll or to any of the auxiliary files.

- `src/counter.tact` — source code of the Counter contract
- `src/counter.spec.ts` — test suite for it
- `src/poll.tact` — source code of the Open-ended Poll contract
- `src/poll.spec.ts` — test suite for it
- `deploy.ts` – script for deploying the contracts
- `tact.config.json` – compiler settings

To add new contract files to the project, do:

1. Create a new `src/my-new-contract.tact`
2. Copy an existing test suite into the one for the new contract in `src/my-new-contract.spec.ts`, then adjust the contents to match the new contract
3. If the contract is used directly and not deployed by other ones, then:
* Modify `deploy.ts`
* Modify `tact.config.json`

To build, test or deploy contracts see the following commands.

## Commands

- `pmRun build` – build contracts and the `.ts` wrappers
- `pmRun test` – build contracts and run jest tests
- `pmRun fmt` – fix source code formatting
- `pmRun lint` – run semantic checks with `misti` linter
- `pmRun verifier:testnet` – deploy contract to testnet
- `pmRun verifier:mainnet` – deploy contract to mainnet
- `pmRun fmt:check` – check source code formatting (for CI)

## Available CLIs

- `tact` – Tact compiler
- `tact-fmt` – Tact formatter
- `unboc` – Disassembler
- `@nowarp/misti` – Misti static analyzer
- `jest` – Jest testing framework

Use `npx` to run any of the CLIs available. For example, to invoke the Tact formatter, execute this:

```shell
npx tact-fmt
```

## Learn more about Tact

- [Website](https://tact-lang.org/)
- [Documentation](https://docs.tact-lang.org/)
- [Learn Tact in Y minutes](https://docs.tact-lang.org/book/learn-tact-in-y-minutes/)
- [Debugging and testing Tact contracts](https://docs.tact-lang.org/book/debug/)
- [Gas best practices](https://docs.tact-lang.org/book/gas-best-practices/)
- [Security best practices](https://docs.tact-lang.org/book/security-best-practices/)
- [Awesome Tact](https://github.com/tact-lang/awesome-tact)

For more interesting contract examples, see the [Tact's DeFi Cookbook](https://github.com/tact-lang/defi-cookbook).

## Community

If you can’t find the answer in the [docs](https://docs.tact-lang.org), or you’ve tried to do some local testing and it still didn’t help — don’t hesitate to reach out to Tact’s flourishing community:

- [`@tactlang` on Telegram](https://t.me/tactlang) - Main community chat and discussion group.
- [`@tactlang_ru` on Telegram](https://t.me/tactlang_ru) _(Russian)_
- [`@tact_kitchen` on Telegram](https://t.me/tact_kitchen) - Channel with updates from the team.
- [`@tact_language` on X/Twitter](https://x.com/tact_language)
- [`tact-lang` organization on GitHub](https://github.com/tact-lang)
- [`@ton_studio` on Telegram](https://t.me/ton_studio)
- [`@thetonstudio` on X/Twitter](https://x.com/thetonstudio)

Good luck on your coding adventure with ⚡ Tact!
52 changes: 52 additions & 0 deletions template/quick-start/deploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { resolve } from "path";
import { readFile } from "fs/promises";
import { contractAddress } from "@ton/core";
import { prepareTactDeployment } from "@tact-lang/deployer";
import { Counter } from "./output/counter_Counter";
import { Poll } from "./output/poll_Poll";

async function main() {
console.log("Deploying...");
const toProduction = process.argv.length === 3 && process.argv[2] === "mainnet";

// 1. Counter contract
{
// Default (initial) values of persistent state variables are supplied here
const init = await Counter.init(0n, 0n);

// Obtaining a convenient link to deploy a new contract in the mainnet or testnet,
// which could be used by your existing Toncoin wallet.
const prepare = await prepareTactDeployment({
// The .pkg file is a special JSON file containing the compiled contract,
// its dependencies, and all related metadata.
//
// See: https://docs.tact-lang.org/ref/evolution/otp-006/
pkg: await readFile(resolve(__dirname, "output", "counter_Counter.pkg")),
data: init.data.toBoc(),
testnet: !toProduction,
});

// Contract addresses on TON are obtained deterministically,
// from the initial code and data. The most used chain is basechain,
// which is the workchain with ID 0.
const address = contractAddress(0, init).toString({ testOnly: !toProduction });
console.log(`Contract address: ${address}`);
console.log(`Please, follow deployment link: ${prepare}`);
}

// 2. Poll contract — the procedure is the same,
// but persistent state variables and contract output would differ.
{
const init = await Poll.init(0n, 0n, 0n);
const prepare = await prepareTactDeployment({
pkg: await readFile(resolve(__dirname, "output", "poll_Poll.pkg")),
data: init.data.toBoc(),
testnet: !toProduction,
});
const address = contractAddress(0, init).toString({ testOnly: !toProduction });
console.log(`Contract address: ${address}`);
console.log(`Please, follow deployment link: ${prepare}`);
}
}

void main();
9 changes: 9 additions & 0 deletions template/quick-start/gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
node_modules
dist
output
.idea/
.DS_Store/
*.swp
.helix/
.vim/
.nvim/
Loading