-
Notifications
You must be signed in to change notification settings - Fork 229
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #245 from Telegram-Mini-Apps/feature/scaffold-cli
CLI to scaffold a mini application
- Loading branch information
Showing
13 changed files
with
604 additions
and
172 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,3 @@ | ||
module.exports = { | ||
extends: ['custom/base'], | ||
}; |
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,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" | ||
} | ||
} |
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,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(); |
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,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); | ||
}); | ||
}); | ||
} |
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,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)); | ||
} |
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,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)); | ||
}); | ||
}); | ||
} |
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,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; | ||
}); | ||
} |
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,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); | ||
} |
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,10 @@ | ||
{ | ||
"extends": "tsconfig/esnext.json", | ||
"compilerOptions": { | ||
"module": "NodeNext", | ||
"types": ["node"] | ||
}, | ||
"include": [ | ||
"src" | ||
] | ||
} |
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,13 @@ | ||
import { defineConfig } from 'vite'; | ||
|
||
export default defineConfig({ | ||
build: { | ||
emptyOutDir: true, | ||
ssr: true, | ||
lib: { | ||
entry: 'src/index.ts', | ||
formats: ['cjs'], | ||
fileName: 'index', | ||
}, | ||
}, | ||
}); |
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
Oops, something went wrong.