diff --git a/packages/create-logto/package.json b/packages/create-logto/package.json new file mode 100644 index 000000000..12c5778b3 --- /dev/null +++ b/packages/create-logto/package.json @@ -0,0 +1,51 @@ +{ + "name": "create-logto", + "version": "1.0.0", + "description": "Logto creation to getting started.", + "author": "Silverhand Inc. ", + "homepage": "https://github.com/logto-io/logto#readme", + "license": "MPL-2.0", + "files": [ + "lib" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/logto-io/logto.git" + }, + "bin": "./lib/index.js", + "scripts": { + "precommit": "lint-staged", + "build": "rimraf lib && tsc", + "dev": "tsc --watch --preserveWatchOutput --incremental", + "lint": "eslint --ext .ts src", + "lint:report": "pnpm lint --format json --output-file report.json", + "prepack": "pnpm build" + }, + "engines": { + "node": "^16.0.0" + }, + "bugs": { + "url": "https://github.com/logto-io/logto/issues" + }, + "dependencies": { + "axios": "^0.27.2", + "decompress": "^4.2.1", + "prompts": "^2.4.2" + }, + "devDependencies": { + "@silverhand/eslint-config": "1.0.0", + "@silverhand/ts-config": "1.0.0", + "@types/axios": "^0.14.0", + "@types/decompress": "^4.2.4", + "@types/prompts": "^2.0.14", + "eslint": "^8.21.0", + "lint-staged": "^13.0.0", + "prettier": "^2.7.1", + "rimraf": "^3.0.2", + "typescript": "^4.7.4" + }, + "eslintConfig": { + "extends": "@silverhand" + }, + "prettier": "@silverhand/eslint-config/.prettierrc" +} diff --git a/packages/create-logto/src/functions.ts b/packages/create-logto/src/functions.ts new file mode 100644 index 000000000..b7c066879 --- /dev/null +++ b/packages/create-logto/src/functions.ts @@ -0,0 +1,31 @@ +import { execSync } from 'child_process'; +import { createWriteStream } from 'fs'; + +import axios from 'axios'; + +export const isVersionGreaterThan = (version: string, targetMajor: number) => + Number(version.split('.')[0]) >= targetMajor; + +export const trimV = (version: string) => (version.startsWith('v') ? version.slice(1) : version); + +export const safeExecSync = (command: string) => { + try { + return execSync(command, { encoding: 'utf8', stdio: 'pipe' }); + } catch {} +}; + +export const downloadFile = async (url: string, destination: string) => { + const file = createWriteStream(destination); + const response = await axios.get(url, { responseType: 'stream' }); + response.data.pipe(file); + + return new Promise((resolve, reject) => { + file.on('error', (error) => { + reject(error.message); + }); + file.on('finish', () => { + file.close(); + resolve(file); + }); + }); +}; diff --git a/packages/create-logto/src/index.ts b/packages/create-logto/src/index.ts new file mode 100644 index 000000000..07af84f90 --- /dev/null +++ b/packages/create-logto/src/index.ts @@ -0,0 +1,95 @@ +import { execSync } from 'child_process'; +import { existsSync } from 'fs'; +import { unlink } from 'fs/promises'; +import path from 'path'; + +import decompress from 'decompress'; +import prompt from 'prompts'; + +import { downloadFile, isVersionGreaterThan, safeExecSync, trimV } from './functions'; + +const DIRECTORY = 'logto'; +const NODE_MAJOR_VERSION = 16; +const POSTGRES_MAJOR_VERSION = 14; + +async function main() { + const nodeVersion = execSync('node -v', { encoding: 'utf8', stdio: 'pipe' }); + const pgOutput = safeExecSync('postgres --version') ?? ''; + const pgArray = pgOutput.split(' '); + const pgVersion = pgArray[pgArray.length - 1]!; + + if (!isVersionGreaterThan(trimV(nodeVersion), NODE_MAJOR_VERSION)) { + throw new Error(`Logto requires NodeJS >= ${NODE_MAJOR_VERSION}.0.0.`); + } + + let response; + + try { + response = await prompt( + [ + { + name: 'instancePath', + message: 'Where should we create your logto instance?', + type: 'text', + initial: './' + DIRECTORY, + format: (value: string) => path.resolve(value.trim()), + validate: (value: string) => + existsSync(value) ? 'That path already exists, please try another.' : true, + }, + { + name: 'hasPostgresUrl', + message: `Logto requires PostgreSQL >= ${POSTGRES_MAJOR_VERSION}.0.0 but cannot find in the current environment. Do you have a remote PostgreSQL instance ready?`, + type: !isVersionGreaterThan(trimV(pgVersion), POSTGRES_MAJOR_VERSION) ? 'confirm' : null, + initial: true, + }, + { + name: 'postgresUrl', + message: 'What is the URL of your PostgreSQL instance?', + type: (_, data) => (data.hasPostgresUrl ? 'text' : null), + format: (value: string) => value.trim(), + validate: (value: string) => + (value && + Boolean( + /^(?:([^\s#/:?]+):\/{2})?(?:([^\s#/?@]+)@)?([^\s#/?]+)?(?:\/([^\s#?]*))?(?:\?([^\s#]+))?\S*$/.test( + value + ) + )) || + 'Please enter a valid connection URL.', + }, + { + name: 'startInstance', + message: 'Would you like to start Logto now?', + type: 'confirm', + initial: true, + }, + ], + { + onCancel: () => { + throw new Error('Operation cancelled'); + }, + } + ); + } catch (error: any) { + console.log(error.message); + + return; + } + + const startCommand = `cd ${response.instancePath} && npm start`; + const tarFileLocation = path.resolve('./logto.tar.gz'); + + await downloadFile( + 'https://github.com/logto-io/logto/releases/latest/download/logto.tar.gz', + tarFileLocation + ); + await decompress(tarFileLocation, response.instancePath); + await unlink(tarFileLocation); + + if (response.startInstance) { + execSync(startCommand, { stdio: 'inherit' }); + } else { + console.log(`You can use ${startCommand} to start Logto. Happy hacking!`); + } +} + +main(); diff --git a/packages/create-logto/tsconfig.json b/packages/create-logto/tsconfig.json new file mode 100644 index 000000000..ec160f030 --- /dev/null +++ b/packages/create-logto/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "@silverhand/ts-config/tsconfig.base", + "compilerOptions": { + "outDir": "lib", + "declaration": true + }, + "include": [ + "src" + ] +}