Skip to content

Commit

Permalink
feat: add create-typink package (#127)
Browse files Browse the repository at this point in the history
* Setup package

* Update package.json

* Add basic app flows

* Rename package, add template, wallet connector, and network options

* Update README.md

* Add TODOs

* Add prettier format task, refactor files

* Fix networks need to be an array

* Fix yarn prettify

* Force at least one network to be choose

* Add handler for template files

* Refactoring 🔧

* run directly via tsx

* Deploy contract in astar and alephzero

* Handle preset contract option

* Refactoring

* Handle wallet connector options

* Fix arg not capture `--template` value

* Not create files if template render nothing

* Remove `--version` arg, `default` is default value of `--template`

* Fix `bin` import

* Use `validate-npm-package-name` instead of regex

* Refactoring 🔧🔧

* Fix imports

* Github Action first run expect to be failed

* Update template for wallet connector options

* Github Action for testing

* Refactoring 🔧

* require node > v20

* revert changes

* update dapp styling

* add version/author

* refactoring files

* fix esm

---------

Co-authored-by: Thang X. Vu <thang@coongcrafts.io>
  • Loading branch information
1cedrus and sinzii authored Jan 26, 2025
1 parent c6bc16d commit c54c7b9
Show file tree
Hide file tree
Showing 90 changed files with 6,776 additions and 11 deletions.
60 changes: 60 additions & 0 deletions .github/workflows/create-typink-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Create Typink Tests

on:
push:
workflow_dispatch:
merge_group:

jobs:
create-typink-tests:
runs-on: ubuntu-latest

strategy:
matrix:
node-version: [20.x]

steps:
- uses: actions/checkout@v3
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn install --immutable
- run: yarn build
- name: Create app with create-typink (Default)
run: |
export YARN_ENABLE_IMMUTABLE_INSTALLS=false
cd ..
node ./typink/packages/create-typink/dist/bin/create-typink.mjs --no-git -n typink-app-default -t default -p greeter -N "Pop Testnet" -N "Aleph Zero Testnet" -w Default
ls -la
cd ./typink-app-default
ls -la
- name: Try to build (Default)
run: |
cd ../typink-app-default
yarn build
- name: Create app with create-typink (SubConnect V2)
run: |
export YARN_ENABLE_IMMUTABLE_INSTALLS=false
cd ..
node ./typink/packages/create-typink/dist/bin/create-typink.mjs --no-git -n typink-app-subconnect-v2 -t default -p greeter -N "Pop Testnet" -N "Aleph Zero Testnet" -w "SubConnect V2"
ls -la
cd ./typink-app-subconnect-v2
ls -la
- name: Try to build (SubConnect V2)
run: |
cd ../typink-app-subconnect-v2
yarn build
- name: Create app with create-typink (Talisman Connect)
run: |
export YARN_ENABLE_IMMUTABLE_INSTALLS=false
cd ..
node ./typink/packages/create-typink/dist/bin/create-typink.mjs --no-git -n typink-app-talisman-connect -t default -p greeter -N "Pop Testnet" -N "Aleph Zero Testnet" -w "Talisman Connect"
ls -la
cd ./typink-app-talisman-connect
ls -la
- name: Try to build (Talisman Connect)
run: |
cd ../typink-app-talisman-connect
yarn build
26 changes: 25 additions & 1 deletion examples/demo/src/contracts/deployments.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { alephZeroTestnet, ContractDeployment, popTestnet } from 'typink';
import { alephZeroTestnet, ContractDeployment, popTestnet, alephZero, astar } from 'typink';
import greeterMetadata from './artifacts/greeter/greeter.json';
import psp22Metadata from './artifacts/psp22/psp22.json';

Expand All @@ -22,6 +22,18 @@ export const greeterDeployments: ContractDeployment[] = [
network: alephZeroTestnet.id,
address: '5CDia8Y46K7CbD2vLej2SjrvxpfcbrLVqK2He3pTJod2Eyik',
},
{
id: ContractId.GREETER,
metadata: greeterMetadata as any,
network: alephZero.id,
address: '5CYZtKBxuva33JREQkbeaE4ed2niWb1ijS4pgXbFD61yZti1',
},
{
id: ContractId.GREETER,
metadata: greeterMetadata as any,
network: astar.id,
address: 'WejJavPYsGgcY8Dr5KQSJrTssxUh5EbeYiCfdddeo5aTbse',
},
];

export const psp22Deployments: ContractDeployment[] = [
Expand All @@ -37,6 +49,18 @@ export const psp22Deployments: ContractDeployment[] = [
network: alephZeroTestnet.id,
address: '5G5moUCkx5E2TD3CcRWvweg7rpCLngRmwukuKdaohvfBBmXr',
},
{
id: ContractId.PSP22,
metadata: psp22Metadata as any,
network: alephZero.id,
address: '5EkDPuyLdubc4uUmEhzMFRtcNtmxSoUecfcc9wWLUR7ZFXbb',
},
{
id: ContractId.PSP22,
metadata: psp22Metadata as any,
network: astar.id,
address: 'YFhgALp2qPtPe1pTFereqqB4RUXKzVM7YtCYfqQX412GWHr',
},
];

export const deployments = [...greeterDeployments, ...psp22Deployments];
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"vitest": "^2.1.8"
},
"engines": {
"node": ">=18"
"node": ">=20"
},
"license": "MIT"
}
21 changes: 21 additions & 0 deletions packages/create-typink/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2024 Thang X. Vu <thang@dedot.dev>

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions packages/create-typink/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# create-typink

A CLI tool to create a new project using Typink
4 changes: 4 additions & 0 deletions packages/create-typink/bin/create-typink.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env node
import { createTypink } from '../index.js';

createTypink();
31 changes: 31 additions & 0 deletions packages/create-typink/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "create-typink",
"version": "0.0.0",
"author": "Tung Vu <tung@dedot.dev>",
"main": "src/index.ts",
"type": "module",
"scripts": {
"build": "tsc --project tsconfig.build.json && cp -R ./bin ./dist && cp -R ./src/templates ./dist",
"clean": "rm -rf ./dist && rm -rf ./tsconfig.tsbuildinfo ./tsconfig.build.tsbuildinfo"
},
"bin": "./bin/create-typink.mjs",
"dependencies": {
"arg": "^5.0.2",
"chalk": "^5.4.1",
"ejs": "^3.1.10",
"execa": "^9.5.2",
"inquirer": "^12.3.2",
"listr2": "^8.2.5",
"prettier": "^3.4.2",
"validate-npm-package-name": "^6.0.0"
},
"publishConfig": {
"access": "public",
"directory": "dist"
},
"license": "MIT",
"devDependencies": {
"@types/ejs": "^3",
"@types/validate-npm-package-name": "^4.0.2"
}
}
52 changes: 52 additions & 0 deletions packages/create-typink/src/createProject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Options } from './types.js';
import { Listr } from 'listr2';
import { fileURLToPath } from 'url';
import * as path from 'path';
import chalk from 'chalk';
import {
createProjectDirectory,
createFirstCommit,
copyTemplateFiles,
prettierFormat,
installPackages,
} from './tasks/index.js';

export async function createProject(options: Options) {
const { projectName, skipInstall, noGit } = options;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const templateDirectory = path.resolve(__dirname, './templates');
const targetDirectory = path.resolve(process.cwd(), projectName!);

const tasks = new Listr(
[
{
title: `📁 Create project directory ${targetDirectory}`,
task: () => createProjectDirectory(projectName!),
},
{
title: `🚀 Creating a new Typink app in ${chalk.green.bold(projectName)}`,
task: () => copyTemplateFiles(options, templateDirectory, targetDirectory),
},
{
title: '📦 Installing dependencies with yarn, this could take a while',
task: () => installPackages(targetDirectory),
skip: skipInstall,
},
{
title: '🧹 Formatting the code with Prettier',
task: () => prettierFormat(targetDirectory, options),
},
{
title: `🚨 Create the very first Git commit`,
task: () => createFirstCommit(targetDirectory),
skip: noGit,
},
],
{ rendererOptions: { suffixSkips: true }, exitOnError: true },
);

await tasks.run();
}
38 changes: 38 additions & 0 deletions packages/create-typink/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import chalk from 'chalk';
import {
parseArguments,
renderIntroArt,
promptMissingOptions,
renderHelpMessage,
renderOutroMessage,
} from './utils/index.js';
import { createProject } from './createProject.js';
import { fileURLToPath } from 'url';

export async function createTypink() {
try {
renderIntroArt();

const args = parseArguments();

if (args.help) {
renderHelpMessage();
return;
}

const options = await promptMissingOptions(args);

await createProject(options);

renderOutroMessage(options);
} catch (error) {
console.error(chalk.red.bold('🚨 An error occurred:'), error);
console.error(chalk.red.bold('🚨 Sorry, exiting...'));
}
}

// run directly from root folder: tsx ./packages/create-typink/src/index.ts
const __filename = fileURLToPath(import.meta.url);
if (process.argv[1] === __filename) {
createTypink().catch(console.error);
}
83 changes: 83 additions & 0 deletions packages/create-typink/src/tasks/copyTemplateFiles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { execa } from 'execa';
import { Options } from '../types.js';
import * as fs from 'fs';
import * as ejs from 'ejs';
import * as path from 'path';
import { stringCamelCase } from '@dedot/utils';
import { IS_IGNORE_FILES, IS_TEMPLATE_FILE } from '../utils/index.js';

export async function copyTemplateFiles(options: Options, templatesDir: string, targetDir: string) {
const { projectName, noGit, template } = options;

const templateDir = `${templatesDir}/${template}`;

if (!fs.existsSync(templateDir)) {
throw new Error(`Template directory not found: ${templateDir}`);
}

await fs.promises.cp(templateDir, targetDir, { recursive: true });

const packageJsonPath = `${targetDir}/package.json`;
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));

packageJson.name = projectName;
await fs.promises.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2));

processPresetContract(options, targetDir);
processTemplateFiles(options, targetDir);

if (!noGit) {
await execa('git', ['init'], { cwd: targetDir });
await execa('git', ['checkout', '-b', 'main'], { cwd: targetDir });
}
}

export async function processPresetContract(options: Options, targetDir: string) {
const dirsToCheck = [`${targetDir}/contracts/artifacts`, `${targetDir}/contracts/types`];

dirsToCheck.forEach(async (dir) => {
for (const file of await fs.promises.readdir(dir, { withFileTypes: true })) {
if (file.name === options.presetContract) {
continue;
}

await fs.promises.rm(path.join(dir, file.name), { recursive: true });
}
});
}

export async function processTemplateFiles(rawOptions: Options, targetDir: string) {
const options = {
...rawOptions,
networks: rawOptions.networks?.map(stringCamelCase),
};

await processTemplateFilesRecursive(options, targetDir);
}

async function processTemplateFilesRecursive(options: any, dir: string) {
if (IS_IGNORE_FILES.test(dir)) {
return;
}

const files = await fs.promises.readdir(dir, { withFileTypes: true });

for (const file of files) {
const filePath = path.join(dir, file.name);

if (file.isDirectory()) {
await processTemplateFilesRecursive(options, filePath);
} else {
if (IS_TEMPLATE_FILE.test(filePath)) {
const content = fs.readFileSync(filePath, 'utf-8');
const result = ejs.render(content, { options });

if (result.trim() !== '') {
await fs.promises.writeFile(filePath.replace('.template.ejs', ''), result);
}

await fs.promises.rm(filePath);
}
}
}
}
8 changes: 8 additions & 0 deletions packages/create-typink/src/tasks/createFirstCommit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { execa } from 'execa';

export async function createFirstCommit(targetDirectory: string) {
await execa('git', ['add', '.'], { cwd: targetDirectory });
await execa('git', ['commit', '-m', 'Initial commit 🚀'], {
cwd: targetDirectory,
});
}
13 changes: 13 additions & 0 deletions packages/create-typink/src/tasks/createProjectDirectory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { execa } from 'execa';

export async function createProjectDirectory(projectName: string) {
try {
const result = await execa('mkdir', [projectName]);

if (result.failed) {
throw new Error(`There was an error when running mkdir command`);
}
} catch (error: any) {
throw new Error(`Failed to create project directory: ${projectName} with error: ${error.message}`);
}
}
5 changes: 5 additions & 0 deletions packages/create-typink/src/tasks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './createProjectDirectory.js';
export * from './copyTemplateFiles.js';
export * from './createFirstCommit.js';
export * from './installPackages.js';
export * from './prettierFormat.js';
5 changes: 5 additions & 0 deletions packages/create-typink/src/tasks/installPackages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { execa } from 'execa';

export async function installPackages(targetDirectory: string) {
await execa('yarn', ['install'], { cwd: targetDirectory });
}
Loading

0 comments on commit c54c7b9

Please sign in to comment.