Skip to content

Commit

Permalink
Merge pull request #245 from Telegram-Mini-Apps/feature/scaffold-cli
Browse files Browse the repository at this point in the history
CLI to scaffold a mini application
  • Loading branch information
heyqbnk authored Mar 8, 2024
2 parents c64fc68 + e812efe commit ed6e0ec
Show file tree
Hide file tree
Showing 13 changed files with 604 additions and 172 deletions.
3 changes: 3 additions & 0 deletions packages/create-mini-app/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: ['custom/base'],
};
50 changes: 50 additions & 0 deletions packages/create-mini-app/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@tma.js/create-mini-app",
"version": "0.0.1",
"description": "",
"author": "Vladislav Kibenko <[email protected]>",
"homepage": "https://github.com/Telegram-Mini-Apps/tma.js#readme",
"repository": {
"type": "git",
"url": "[email protected]:Telegram-Mini-Apps/tma.js.git",
"directory": "packages/create-mini-app"
},
"bugs": {
"url": "https://github.com/Telegram-Mini-Apps/tma.js/issues"
},
"keywords": [
"telegram-mini-apps",
"templates",
"boilerplates"
],
"license": "MIT",
"type": "module",
"sideEffects": false,
"files": [
"dist"
],
"bin": "dist/index.cjs",
"scripts": {
"lint": "eslint src",
"lint:fix": "pnpm run lint --fix",
"typecheck": "tsc --noEmit -p tsconfig.json",
"build": "vite build --config vite.config.ts"
},
"devDependencies": {
"@types/inquirer": "^9.0.7",
"@types/node": "^20.0.0",
"build-utils": "workspace:*",
"eslint-config-custom": "workspace:*",
"test-utils": "workspace:*",
"tsconfig": "workspace:*"
},
"publishConfig": {
"access": "public"
},
"dependencies": {
"chalk": "^5.3.0",
"commander": "^12.0.0",
"inquirer": "^9.2.15",
"ora": "^8.0.1"
}
}
76 changes: 76 additions & 0 deletions packages/create-mini-app/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import process from 'node:process';

import chalk from 'chalk';
import { program } from 'commander';

import { isGitInstalled } from './isGitInstalled.js';
import { promptRootDir } from './promptRootDir.js';
import { spawnWithSpinner } from './spawnWithSpinner.js';
import { filterTemplates } from './templates.js';
import packageJson from '../package.json';

const { bold, green, blue, red } = chalk;

program
.name(packageJson.name)
.description(packageJson.description)
.version(packageJson.version)
.action(async () => {
// Check if git is installed.
if (!await isGitInstalled()) {
console.error('To run this CLI tool, git should be installed. Installation guide: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git');
process.exit(1);
}

// Prompt the project root directory name.
let rootDir: string | null = null;
while (!rootDir) {
console.clear();
rootDir = await promptRootDir();
}

const [{ repository: { clone, link } }] = filterTemplates('ts', 'tma.js', 'react');

// Clone the template.
try {
await spawnWithSpinner({
title: `Cloning the template from GitHub: ${bold(blue(link))}`,
command: `git clone ${clone} ${rootDir}`,
titleFail(outputOrCode) {
return `Failed to load the template. ${
typeof outputOrCode === 'string'
? `Error: ${red(outputOrCode)}`
: `Error code: ${red(outputOrCode)}`
}`;
},
titleSuccess: `Cloned the template: ${bold(blue(link))}`,
});
} catch {
process.exit(1);
}

// Remove the .git folder.
try {
await spawnWithSpinner({
title: 'Removing the .git folder',
command: `rm -rf ${rootDir}/.git`,
titleFail(outputOrCode) {
return `Failed to delete .git directory ${
typeof outputOrCode === 'string'
? `Error: ${red(outputOrCode)}`
: `Error code: ${red(outputOrCode)}`
}`;
},
titleSuccess: '.git folder removed',
});
} catch {
process.exit(1);
}

console.log([
green(bold('Your project has been successfully initialized!')),
`Now, open the ${bold(rootDir)} directory and follow the instructions presented in the ${bold('.README')} file.`,
].join('\n'));
});

program.parse();
12 changes: 12 additions & 0 deletions packages/create-mini-app/src/isGitInstalled.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { spawn } from 'node:child_process';

/**
* @returns Promise with true if git is currently installed.
*/
export function isGitInstalled(): Promise<boolean> {
return new Promise((res) => {
spawn('git --version', { shell: true }).on('exit', (code) => {
res(!code);
});
});
}
48 changes: 48 additions & 0 deletions packages/create-mini-app/src/promptRootDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { existsSync } from 'node:fs';
import { basename } from 'node:path';

import chalk from 'chalk';
import inquirer from 'inquirer';

import { toAbsolute } from './toAbsolute.js';

interface Answers {
rootDir: string;
confirmed: boolean;
}

/**
* Prompts current user project root directory.
* @returns Directory name.
*/
export async function promptRootDir(): Promise<string | null> {
return inquirer
.prompt<Answers>([{
type: 'input',
name: 'rootDir',
message: 'Enter the directory name:',
validate(value) {
if (value.length === 0) {
return 'Directory name should not be empty.';
}

const dir = toAbsolute(value);
if (existsSync(dir)) {
return `Directory already exists: ${dir}`;
}

if (value !== basename(value)) {
return `Value "${value}" contains invalid symbols.`;
}

return true;
},
}, {
type: 'confirm',
name: 'confirmed',
message({ rootDir }) {
return `Is this your desired directory? ${chalk.green(toAbsolute(rootDir))}`;
},
}])
.then(({ rootDir, confirmed }) => (confirmed ? rootDir : null));
}
74 changes: 74 additions & 0 deletions packages/create-mini-app/src/spawnWithSpinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { spawn } from 'node:child_process';

import chalk from 'chalk';
import ora from 'ora';

interface Options {
/**
* Shell command to run.
*/
command: string;
/**
* Spinner title.
*/
title: string;
/**
* Text displayed when process completed successfully.
*/
titleSuccess?: string;
/**
* Text displayed when process failed.
*/
titleFail?: string | ((outputOrCode: string | number) => string);
}

/**
* Spawns new process with spinner.
* @param options - function options.
*/
export function spawnWithSpinner(options: Options): Promise<void> {
const {
command,
title,
titleFail,
titleSuccess,
} = options;
const spinner = ora({ text: title, hideCursor: false }).start();

return new Promise((res, rej) => {
const proc = spawn(command, { shell: true });

// Buffer which contains error data.
let errBuf = Buffer.from([]);

// When something was received from the error channel, we append it to the final buffer.
proc.stderr.on('data', (buf) => {
errBuf = Buffer.concat([errBuf, buf]);

// Update the spinner text to let user know, process is working.
spinner.suffixText = chalk.bgGray(chalk.italic(buf.toString()));
});

proc.on('exit', (code) => {
// Drop process outputs.
spinner.suffixText = '';

// If error code was not returned. It means, process completed successfully.
if (!code) {
spinner.succeed(titleSuccess);
res();
return;
}

const errString = errBuf.length ? errBuf.toString() : `Action error code: ${code}`;
const errorMessage = titleFail
? typeof titleFail === 'string'
? titleFail
: titleFail(errString)
: errString;

spinner.fail(errorMessage);
rej(new Error(errorMessage));
});
});
}
37 changes: 37 additions & 0 deletions packages/create-mini-app/src/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
interface CreateTemplate<Language extends string, Framework extends string> {
sdk: 'telegram' | 'tma.js';
language: Language;
framework: Framework;
repository: {
clone: string;
link: string;
};
}

type JsTemplate = CreateTemplate<'js', 'solid' | 'react' | 'next' | 'vanilla js'>;
type TsTemplate = CreateTemplate<'ts', 'solid' | 'react' | 'next'>;
type AnyTemplate = JsTemplate | TsTemplate;

type AllowedLanguage = AnyTemplate['language'];
type AllowedSDK = AnyTemplate['sdk'];
type AllowedFramework = AnyTemplate['framework'];

export const templates: AnyTemplate[] = [{
language: 'ts',
sdk: 'tma.js',
framework: 'react',
repository: {
clone: '[email protected]:Telegram-Mini-Apps/reactjs-template.git',
link: 'github.com/Telegram-Mini-Apps/reactjs-template',
},
}];

export function filterTemplates(
language: AllowedLanguage,
sdk: AllowedSDK,
framework: AllowedFramework,
): AnyTemplate[] {
return templates.filter((t) => {
return t.sdk === sdk && t.language === language && t.framework === framework;
});
}
10 changes: 10 additions & 0 deletions packages/create-mini-app/src/toAbsolute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { isAbsolute, resolve } from 'node:path';
import process from 'node:process';

/**
* Converts specified absolute or relative path to absolute.
* @param path - path to convert.
*/
export function toAbsolute(path: string): string {
return isAbsolute(path) ? path : resolve(process.cwd(), path);
}
10 changes: 10 additions & 0 deletions packages/create-mini-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "tsconfig/esnext.json",
"compilerOptions": {
"module": "NodeNext",
"types": ["node"]
},
"include": [
"src"
]
}
13 changes: 13 additions & 0 deletions packages/create-mini-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { defineConfig } from 'vite';

export default defineConfig({
build: {
emptyOutDir: true,
ssr: true,
lib: {
entry: 'src/index.ts',
formats: ['cjs'],
fileName: 'index',
},
},
});
3 changes: 3 additions & 0 deletions packages/eslint-config-custom/rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ module.exports = {
// We select line endings depending on current OS.
// See: https://stackoverflow.com/q/39114446/2771889
'linebreak-style': ['error', (process.platform === 'win32' ? 'windows' : 'unix')],
'no-await-in-loop': 0,
'no-console': 0,
'no-continue': 0,
'no-nested-ternary': 0,
// Sometimes we need to write "void promise".
'no-void': 0,
'object-curly-newline': ['error', { consistent: true }],
Expand Down
2 changes: 1 addition & 1 deletion packages/init-data-node/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
"build": "vite build"
},
"devDependencies": {
"@types/node": "^16.0.0",
"@types/node": "^20.0.0",
"build-utils": "workspace:*",
"eslint-config-custom": "workspace:*",
"tsconfig": "workspace:*"
Expand Down
Loading

0 comments on commit ed6e0ec

Please sign in to comment.