From 4a82829c820d7508944c0986ce2f5267c8f9e41a Mon Sep 17 00:00:00 2001 From: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Thu, 30 Mar 2023 01:03:36 +0100 Subject: [PATCH 01/10] wip overhaul of crwa and adding tui package --- packages/create-redwood-app/package.json | 2 - .../src/create-redwood-app.js | 806 +++++++++--------- packages/tui/.babelrc.js | 1 + packages/tui/README.md | 6 + packages/tui/package.json | 44 + packages/tui/src/__tests__/index.test.js | 1 + packages/tui/src/index.ts | 286 +++++++ packages/tui/tsconfig.json | 10 + yarn.lock | 35 +- 9 files changed, 781 insertions(+), 410 deletions(-) create mode 100644 packages/tui/.babelrc.js create mode 100644 packages/tui/README.md create mode 100644 packages/tui/package.json create mode 100644 packages/tui/src/__tests__/index.test.js create mode 100644 packages/tui/src/index.ts create mode 100644 packages/tui/tsconfig.json diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index 8414e169e47d..114158085351 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -29,10 +29,8 @@ "chalk": "4.1.2", "check-node-version": "4.2.1", "core-js": "3.29.1", - "enquirer": "2.3.6", "execa": "5.1.1", "fs-extra": "11.1.1", - "listr2": "5.0.8", "terminal-link": "2.1.1", "yargs": "17.7.1" }, diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js index 254bad0c70e9..bc6a4685ef5b 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -6,60 +6,338 @@ // Usage: // `$ yarn create redwood-app ./path/to/new-project` -import { spawn } from 'child_process' +// import { spawn } from 'child_process' import path from 'path' import chalk from 'chalk' import checkNodeVersion from 'check-node-version' -import { prompt } from 'enquirer' import execa from 'execa' import fs from 'fs-extra' -import { Listr, figures } from 'listr2' import terminalLink from 'terminal-link' import { hideBin } from 'yargs/helpers' import yargs from 'yargs/yargs' +import { RedwoodTUI, styling } from '@redwoodjs/tui' + import { name, version } from '../package' +const tui = new RedwoodTUI() + +async function checkCompatibility(templateDir, yarnInstall) { + tui.setContentMode('text') + tui.setHeader('Checking node and yarn compatibility', { spinner: true }) + + if (!yarnInstall) { + tui.setContent( + ` ${styling.warning( + '⚠ Warning' + )}: Skipped check because yarn install was skipped with "--yarn-install false"` + ) + tui.moveOn() + return + } + + const [engineCheckPassed, engineCheckErrors] = await new Promise( + (resolve) => { + const { engines } = require(path.join(templateDir, 'package.json')) + + // this checks all engine requirements, including Node.js and Yarn + checkNodeVersion(engines, (_error, result) => { + if (result.isSatisfied) { + return resolve([true, []]) + } + const logStatements = Object.keys(result.versions) + .filter((name) => !result.versions[name].isSatisfied) + .map((name) => { + const { version, wanted } = result.versions[name] + return `${name} ${wanted} required, but you have ${version}` + }) + return resolve([false, logStatements]) + }) + } + ) + + if (engineCheckPassed) { + tui.setContent(' ✔ Compatibility checks passed') + tui.moveOn() + return + } + + // TODO: Handle engine check errors + if (!engineCheckPassed) { + const engineCheckErrorDocs = terminalLink( + 'Tutorial - Prerequisites', + 'https://redwoodjs.com/docs/tutorial/chapter1/prerequisites' + ) + tui.displayError( + 'Compatibility checks failed', + [ + ` ${engineCheckErrors.join('\n')}`, + '', + ` This may make your project incompatible with some deploy targets.`, + ` See: ${engineCheckErrorDocs}`, + ].join('\n') + ) + const response = await tui.prompt({ + type: 'select', + name: 'override-engine-error', + message: 'How would you like to proceed?', + choices: ['Override error and continue install', 'Quit install'], + initial: 0, + onCancel: () => process.exit(1), + }) + if (response['override-engine-error'] === 'Quit install') { + process.exit(1) // TODO: Should we use a different exit code? + } + } +} + +async function promptForTypescript() { + tui.setContentMode('text') + const response = await tui.prompt({ + type: 'Select', + name: 'language', + choices: ['TypeScript', 'JavaScript'], + message: 'Select your preferred coding language', + initial: 'TypeScript', + onCancel: () => process.exit(1), + }) + return response.language === 'TypeScript' +} + +async function promptForGit() { + tui.setContentMode('text') + const response = await tui.prompt({ + type: 'Toggle', + name: 'git', + message: 'Do you want to initialize a git repo?', + enabled: 'Yes', + disabled: 'no', + initial: 'Yes', + onCancel: () => process.exit(1), + }) + return response.git +} + +function createProjectFiles(newAppDir, overwrite, yarn1) { + // Check if the new app directory already exists + if (fs.existsSync(newAppDir) && !overwrite) { + // Check if the directory contains files and show an error if it does + if (fs.readdirSync(newAppDir).length > 0) { + tui.setBoxen({ + padding: 1, + borderColor: 'red', + title: '⚠ Project directory already contains files', + titleAlignment: 'left', + }) + tui.setContentMode('text') + tui.setContent( + [ + `'${styling.info(newAppDir)}' already exists and is not empty`, + ``, + `You can use the '${styling.info( + 'overwrite' + )}' flag to create the project even if target directory isn't empty`, + ].join('\n') + ) + tui.disable() + process.exit(1) + } + } + + // Ensure the new app directory exists + fs.ensureDirSync(path.dirname(newAppDir)) + + // Copy the template files to the new app directory + const templateDirectory = path.resolve(__dirname, '../template') + fs.copySync(templateDirectory, newAppDir, { overwrite }) + + // .gitignore is renamed here to force file inclusion during publishing + fs.rename( + path.join(newAppDir, 'gitignore.template'), + path.join(newAppDir, '.gitignore') + ) + + // We need to update some files when the user selects to use yarn v1 + if (yarn1) { + // rm files: + // - .yarnrc.yml + // - .yarn + fs.rmSync(path.join(newAppDir, '.yarnrc.yml')) + fs.rmSync(path.join(newAppDir, '.yarn'), { + recursive: true, + force: true, + }) + + // rm after `.pnp.*` + const gitignore = fs.readFileSync(path.join(newAppDir, '.gitignore'), { + encoding: 'utf-8', + }) + const [yarn1Gitignore, _yarn3Gitignore] = gitignore.split('.pnp.*') + fs.writeFileSync(path.join(newAppDir, '.gitignore'), yarn1Gitignore) + + // rm `packageManager` from package.json + const packageJSON = fs.readJSONSync(path.join(newAppDir, 'package.json')) + delete packageJSON.packageManager + fs.writeJSONSync(path.join(newAppDir, 'package.json'), packageJSON, { + spaces: 2, + }) + } +} + +async function installNodeModules(newAppDir, yarnInstall) { + if (!yarnInstall) { + tui.setContentMode('text') + tui.setContent('Skipping yarn install') + return + } + + tui.setContentMode('text') + tui.setHeader('Installing node modules', { spinner: true }) + tui.setContent(' ⏱ This could take a minute or more...') + + const yarnInstallSubprocess = execa('yarn install', { + shell: true, + cwd: newAppDir, + }) + + try { + // TODO: We didn't want to have the yarn output show up? + // tui.setOutStream(yarnInstallSubprocess.stdout) + // tui.setErrStream(yarnInstallSubprocess.stderr) + await yarnInstallSubprocess + } catch (error) { + tui.displayError( + "Couldn't install node modules", + [ + `We could not install node modules via ${styling.info( + "'yarn install'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + process.exit(1) + } + + tui.setHeader('Node modules successfully installed', { spinner: false }) + tui.setContent('') + tui.moveOn() +} + +async function convertToJavascript(newAppDir) { + tui.setContentMode('text') + tui.setHeader('Converting TypeScript files to JavaScript', { spinner: true }) + + const conversionSubprocess = execa('yarn rw ts-to-js', { + shell: true, + cwd: newAppDir, + }) + + try { + // TODO: We didn't want to have the yarn output show up? + // tui.setOutStream(conversionSubprocess.stdout) + // tui.setErrStream(conversionSubprocess.stderr) + await conversionSubprocess + } catch (error) { + tui.displayError( + "Couldn't convert TypeScript files to JavaScript", + [ + `We could not convert the Typescript files to Javascript using ${styling.info( + "'yarn rw ts-to-js'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + process.exit(1) + } + + tui.setHeader('Converted TypeScript files to JavaScript', { spinner: false }) + tui.moveOn() +} + +async function generateTypes(newAppDir) { + tui.setContentMode('text') + tui.setHeader('Generating types', { spinner: true }) + + const generateSubprocess = execa('yarn rw-gen', { + shell: true, + cwd: newAppDir, + }) + + try { + // TODO: We didn't want to have the yarn output show up? + // tui.setOutStream(conversionSubprocess.stdout) + // tui.setErrStream(conversionSubprocess.stderr) + await generateSubprocess + } catch (error) { + tui.displayError( + "Couldn't generate types", + [ + `We could not generate types using ${styling.info( + "'yarn rw-gen'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + process.exit(1) + } + + tui.setHeader('Generated types', { spinner: false }) + tui.moveOn() +} + +async function initialiseGit(newAppDir) { + tui.setContentMode('text') + tui.setHeader('Initialising a git repo', { spinner: true }) + + const gitSubprocess = execa( + 'git init && git add . && git commit -m "Initial commit"', + { + shell: true, + cwd: newAppDir, + } + ) + + try { + // TODO: We didn't want to have the yarn output show up? + // tui.setOutStream(conversionSubprocess.stdout) + // tui.setErrStream(conversionSubprocess.stderr) + await gitSubprocess + } catch (error) { + tui.displayError( + "Couldn't initialise a git repo", + [ + `We could not initialise a git repo using ${styling.info( + 'git init && git add . && git commit -m "Initial commit"' + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + process.exit(1) + } + + tui.setHeader('Initialied a git repo', { spinner: false }) + tui.moveOn() +} + /** - * To keep a consistent color/style palette between cli packages, such as - * @redwood/create-redwood-app and @redwood/cli, please keep them compatible - * with one and another. We'll might split up and refactor these into a - * separate package when there is a strong motivation behind it. - * - * Current files: - * - * - packages/cli/src/lib/colors.js - * - packages/create-redwood-app/src/create-redwood-app.js (this file) + * This function creates a new RedwoodJS app. * + * It performs the following actions: + * - TODO - Add a list of what this function does */ -;(async () => { - // Styles for terminal - const style = { - error: chalk.bold.red, - warning: chalk.keyword('orange'), - success: chalk.greenBright, - info: chalk.grey, - - header: chalk.bold.underline.hex('#e8e8e8'), - cmd: chalk.hex('#808080'), - redwood: chalk.hex('#ff845e'), - love: chalk.redBright, - - green: chalk.green, - } +async function createRedwoodApp() { + tui.enable() - // Initial welcome message - console.log( - `${style.redwood( - '------------------------------------------------------------------' - )}` - ) - console.log(`🌲⚡️ ${style.header('Welcome to RedwoodJS!')} ⚡️🌲`) - console.log( - `${style.redwood( - '------------------------------------------------------------------' - )}` + // Introductory message + tui.drawLinesAndMoveOn( + `${styling.redwood('-'.repeat(66))}`, + `${' '.repeat(16)}🌲⚡️ ${styling.header('Welcome to RedwoodJS!')} ⚡️🌲`, + `${styling.redwood('-'.repeat(66))}` ) // Extract the args as provided by the user in the command line @@ -68,7 +346,7 @@ import { name, version } from '../package' 'yarn-install': yarnInstall, typescript, overwrite, - telemetry: telemetry, + telemetry, yarn1, 'git-init': gitInit, } = yargs(hideBin(process.argv)) @@ -117,386 +395,106 @@ import { name, version } from '../package' // Throw an error if there is no target directory specified if (!targetDir) { - console.error('Please specify the project directory') - console.log( - ` ${chalk.cyan('yarn create redwood-app')} ${chalk.green( - '' - )}` - ) - console.log() - console.log('For example:') - console.log( - ` ${chalk.cyan('yarn create redwood-app')} ${chalk.green( - 'my-redwood-app' - )}` + tui.displayError( + 'No target directory specified', + [ + 'Please specify the project directory', + ` ${chalk.cyan('yarn create redwood-app')} ${chalk.green( + '' + )}`, + '', + 'For example:', + ` ${chalk.cyan('yarn create redwood-app')} ${chalk.green( + 'my-redwood-app' + )}`, + ].join('\n') ) process.exit(1) } const newAppDir = path.resolve(process.cwd(), targetDir) - const appDirExists = fs.existsSync(newAppDir) const templateDir = path.resolve(__dirname, '../template') - const createProjectTasks = ({ newAppDir, overwrite }) => { - return [ - { - title: `${ - appDirExists ? 'Using' : 'Creating' - } directory '${newAppDir}'`, - task: () => { - if (appDirExists && !overwrite) { - // make sure that the target directory is empty - if (fs.readdirSync(newAppDir).length > 0) { - console.error( - style.error( - `\n'${newAppDir}' already exists and is not empty\n` - ) - ) - process.exit(1) - } - } else { - fs.ensureDirSync(path.dirname(newAppDir)) - } - fs.copySync(templateDir, newAppDir, { overwrite: overwrite }) - // .gitignore is renamed here to force file inclusion during publishing - fs.rename( - path.join(newAppDir, 'gitignore.template'), - path.join(newAppDir, '.gitignore') - ) - }, - }, - { - title: 'Converting to yarn 1', - enabled: () => yarn1, - task: () => { - // rm files: - // - .yarnrc.yml - // - .yarn - fs.rmSync(path.join(newAppDir, '.yarnrc.yml')) - fs.rmSync(path.join(newAppDir, '.yarn'), { - recursive: true, - force: true, - }) + // Engine check + await checkCompatibility(templateDir, yarnInstall) - // rm after `.pnp.*` - const gitignore = fs.readFileSync( - path.join(newAppDir, '.gitignore'), - { - encoding: 'utf-8', - } - ) - const [yarn1Gitignore, _yarn3Gitignore] = gitignore.split('.pnp.*') - fs.writeFileSync(path.join(newAppDir, '.gitignore'), yarn1Gitignore) - - // rm `packageManager` from package.json - const packageJSON = fs.readJSONSync( - path.join(newAppDir, 'package.json') - ) - delete packageJSON.packageManager - fs.writeJSONSync(path.join(newAppDir, 'package.json'), packageJSON, { - spaces: 2, - }) - }, - }, - ] - } + // Determine ts/js preference + const useTypescript = + typescript === null ? await promptForTypescript() : typescript - const installNodeModulesTasks = ({ newAppDir }) => { - return [ - { - title: "Running 'yarn install'... (This could take a while)", - skip: () => { - if (yarnInstall === false) { - return 'skipped on request' - } - }, - task: () => { - return execa('yarn install', { - shell: true, - cwd: newAppDir, - }) - }, - }, - ] - } + // TODO: Git preference + const useGit = gitInit === null ? await promptForGit() : gitInit - const sendTelemetry = ({ error } = {}) => { - // send 'create' telemetry event, or disable for new app - if (telemetry) { - const command = process.argv - // make command show 'create redwood-app [path] --flags' - command.splice(2, 0, 'create', 'redwood-app') - command[4] = '[path]' - - let args = [ - '--root', - newAppDir, - '--argv', - JSON.stringify(command), - '--duration', - Date.now() - startTime, - '--rwVersion', - version, - ] - if (error) { - args = [...args, '--error', `"${error}"`] - } - - spawn(process.execPath, [path.join(__dirname, 'telemetry.js'), ...args], { - detached: process.env.REDWOOD_VERBOSE_TELEMETRY ? false : true, - stdio: process.env.REDWOOD_VERBOSE_TELEMETRY ? 'inherit' : 'ignore', - }).unref() - } else { - fs.appendFileSync( - path.join(newAppDir, '.env'), - 'REDWOOD_DISABLE_TELEMETRY=1\n' - ) - } - } + // Create project files + createProjectFiles(newAppDir, overwrite, yarn1) - const startTime = Date.now() - - // Engine check Listr. Separate Listr to avoid https://github.com/cenk1cenk2/listr2/issues/296 - // Boolean flag - let hasPassedEngineCheck = null - // Array of strings - let engineErrorLog = [] - // Docs link for engine errors - const engineErrorDocsLink = terminalLink( - 'Tutorial - Prerequisites', - 'https://redwoodjs.com/docs/tutorial/chapter1/prerequisites' - ) + // Install the node packages + await installNodeModules(newAppDir, yarnInstall) - await new Listr( - [ - { - title: 'Checking node and yarn compatibility', - skip: () => { - if (yarnInstall === false) { - return 'Warning: skipping check on request' - } - }, - task: () => { - return new Promise((resolve) => { - const { engines } = require(path.join(templateDir, 'package.json')) - - // this checks all engine requirements, including Node.js and Yarn - checkNodeVersion(engines, (_error, result) => { - if (result.isSatisfied) { - hasPassedEngineCheck = true - return resolve() - } - const logStatements = Object.keys(result.versions) - .filter((name) => !result.versions[name].isSatisfied) - .map((name) => { - const { version, wanted } = result.versions[name] - return `${name} ${wanted} required, but you have ${version}` - }) - engineErrorLog = logStatements - hasPassedEngineCheck = false - return resolve() - }) - }) - }, - }, - ], - { rendererOptions: { clearOutput: true } } - ).run() - - // Show a success message if required engines are present - if (hasPassedEngineCheck === true) { - console.log(`${style.success(figures.tick)} Compatibility checks passed`) + // Conditionally convert to javascript + if (!useTypescript && yarnInstall) { + await convertToJavascript(newAppDir) } - // Show an error and prompt if failed engines check - if (hasPassedEngineCheck === false) { - console.log(`${style.error(figures.cross)} Compatibility checks failed`) - console.log( - [ - ` ${style.warning(figures.warning)} ${engineErrorLog.join('\n')}`, - '', - ` This may make your project incompatible with some deploy targets.`, - ` See: ${engineErrorDocsLink}`, - '', - ].join('\n') - ) - // Prompt user for how to proceed - const response = await prompt({ - type: 'select', - name: 'override-engine-error', - message: 'How would you like to proceed?', - choices: ['Override error and continue install', 'Quit install'], - initial: 0, - onCancel: () => process.exit(1), - }) - // Quit the install if user selects this option, otherwise it will proceed - if (response['override-engine-error'] === 'Quit install') { - process.exit(1) - } + // Generate types + if (yarnInstall) { + await generateTypes(newAppDir) } - // Main install Listr - new Listr( - [ - { - title: 'Language preference', - skip: () => typescript !== null, - task: async (ctx, task) => { - ctx.language = await task.prompt({ - type: 'Select', - choices: ['TypeScript', 'JavaScript'], - message: 'Select your preferred coding language', - initial: 'TypeScript', - }) - task.output = ctx.language - // Error code and exit if someone has disabled yarn install but selected JavaScript - if (!yarnInstall && ctx.language === 'JavaScript') { - throw new Error( - 'JavaScript transpilation requires running yarn install. Please rerun create-redwood-app without disabling yarn install.' - ) - } - }, - options: { - persistentOutput: true, - }, - }, - { - title: 'Git preference', - skip: () => gitInit !== null, - task: async (ctx, task) => { - ctx.gitInit = await task.prompt({ - type: 'Toggle', - message: 'Do you want to initialize a git repo?', - enabled: 'Yes', - disabled: 'no', - initial: 'Yes', - }) - task.output = ctx.gitInit ? 'Initialize a git repo' : 'Skip' - }, - options: { - persistentOutput: true, - }, - }, - { - title: 'Creating Redwood app', - task: () => new Listr(createProjectTasks({ newAppDir, overwrite })), - }, - { - title: 'Installing packages', - task: () => new Listr(installNodeModulesTasks({ newAppDir })), - }, - { - title: 'Convert TypeScript files to JavaScript', - // Enabled if user selects no to typescript prompt - // Enabled if user specified --no-ts via command line - enabled: (ctx) => - yarnInstall === true && - (typescript === false || ctx.language === 'JavaScript'), - task: () => { - return execa('yarn rw ts-to-js', { - shell: true, - cwd: newAppDir, - }) - }, - }, - { - title: 'Generating types', - skip: () => yarnInstall === false, - task: () => { - return execa('yarn rw-gen', { - shell: true, - cwd: newAppDir, - }) - }, - }, - { - title: 'Initializing a git repo', - enabled: (ctx) => gitInit || ctx.gitInit, - task: () => { - return execa( - 'git init && git add . && git commit -m "Initial commit"', - { - shell: true, - cwd: newAppDir, - } - ) - }, - }, - ], - { - rendererOptions: { collapse: false }, - exitOnError: true, - } + // Initialise git repo + if (useGit) { + await initialiseGit(newAppDir) + } + + // Post install message + tui.drawLinesAndMoveOn( + '', + styling.success('Thanks for trying out Redwood!'), + '', + ` ⚡️ ${styling.redwood( + 'Get up and running fast with this Quick Start guide' + )}: https://redwoodjs.com/docs/quick-start`, + '', + styling.header('Join the Community'), + '', + `${styling.redwood(' ❖ Join our Forums')}: https://community.redwoodjs.com`, + `${styling.redwood(' ❖ Join our Chat')}: https://discord.gg/redwoodjs`, + '', + styling.header('Get some help'), + '', + `${styling.redwood( + ' ❖ Get started with the Tutorial' + )}: https://redwoodjs.com/docs/tutorial`, + `${styling.redwood( + ' ❖ Read the Documentation' + )}: https://redwoodjs.com/docs`, + '', + styling.header('Stay updated'), + '', + `${styling.redwood( + ' ❖ Sign up for our Newsletter' + )}: https://www.redwoodjs.com/newsletter`, + `${styling.redwood( + ' ❖ Follow us on Twitter' + )}: https://twitter.com/redwoodjs`, + '', + `${styling.header(`Become a Contributor`)} ${styling.love('❤')}`, + '', + `${styling.redwood( + ' ❖ Learn how to get started' + )}: https://redwoodjs.com/docs/contributing`, + `${styling.redwood( + ' ❖ Find a Good First Issue' + )}: https://redwoodjs.com/good-first-issue`, + '', + `${styling.header(`Fire it up!`)} 🚀`, + '', + `${styling.redwood(` > ${styling.green(`cd ${targetDir}`)}`)}`, + `${styling.redwood(` > ${styling.green(`yarn rw dev`)}`)}`, + '' ) - .run() - .then(() => { - sendTelemetry() - // zOMG the semicolon below is a real Prettier thing. What?? - // https://prettier.io/docs/en/rationale.html#semicolons - ;[ - '', - style.success('Thanks for trying out Redwood!'), - '', - ` ⚡️ ${style.redwood( - 'Get up and running fast with this Quick Start guide' - )}: https://redwoodjs.com/docs/quick-start`, - '', - style.header('Join the Community'), - '', - `${style.redwood( - ' ❖ Join our Forums' - )}: https://community.redwoodjs.com`, - `${style.redwood(' ❖ Join our Chat')}: https://discord.gg/redwoodjs`, - '', - style.header('Get some help'), - '', - `${style.redwood( - ' ❖ Get started with the Tutorial' - )}: https://redwoodjs.com/docs/tutorial`, - `${style.redwood( - ' ❖ Read the Documentation' - )}: https://redwoodjs.com/docs`, - '', - style.header('Stay updated'), - '', - `${style.redwood( - ' ❖ Sign up for our Newsletter' - )}: https://www.redwoodjs.com/newsletter`, - `${style.redwood( - ' ❖ Follow us on Twitter' - )}: https://twitter.com/redwoodjs`, - '', - `${style.header(`Become a Contributor`)} ${style.love('❤')}`, - '', - `${style.redwood( - ' ❖ Learn how to get started' - )}: https://redwoodjs.com/docs/contributing`, - `${style.redwood( - ' ❖ Find a Good First Issue' - )}: https://redwoodjs.com/good-first-issue`, - '', - `${style.header(`Fire it up!`)} 🚀`, - '', - `${style.redwood(` > ${style.green(`cd ${targetDir}`)}`)}`, - `${style.redwood(` > ${style.green(`yarn rw dev`)}`)}`, - '', - ].map((item) => console.log(item)) - }) - .catch((e) => { - console.log() - console.log(e) - sendTelemetry({ error: e.message }) - - if (fs.existsSync(newAppDir)) { - console.log( - style.warning(`\nWarning: Directory `) + - style.cmd(`'${newAppDir}' `) + - style.warning( - `was created. However, the installation could not complete due to an error.\n` - ) - ) - } - process.exit(1) - }) -})() + tui.disable() +} + +createRedwoodApp() diff --git a/packages/tui/.babelrc.js b/packages/tui/.babelrc.js new file mode 100644 index 000000000000..3b2c815712d9 --- /dev/null +++ b/packages/tui/.babelrc.js @@ -0,0 +1 @@ +module.exports = { extends: '../../babel.config.js' } diff --git a/packages/tui/README.md b/packages/tui/README.md new file mode 100644 index 000000000000..19c9c1aea0bf --- /dev/null +++ b/packages/tui/README.md @@ -0,0 +1,6 @@ +# TUI + +Terminal User Interface for RedwoodJS. A collection of helpful functions to standardise and simplify terminal output. + +## API +**TODO** diff --git a/packages/tui/package.json b/packages/tui/package.json new file mode 100644 index 000000000000..dad8f6925c67 --- /dev/null +++ b/packages/tui/package.json @@ -0,0 +1,44 @@ +{ + "name": "@redwoodjs/tui", + "version": "4.0.0", + "repository": { + "type": "git", + "url": "https://github.com/redwoodjs/redwood.git", + "directory": "packages/tui" + }, + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "yarn build:js && yarn build:types", + "build:js": "babel src -d dist --extensions \".js,.ts,.tsx\"", + "build:types": "tsc --build --verbose", + "build:watch": "nodemon --watch src --ext \"js,ts,tsx\" --ignore dist --exec \"yarn build\"", + "prepublishOnly": "NODE_ENV=production yarn build", + "test": "jest src", + "test:watch": "yarn test --watch" + }, + "jest": { + "testPathIgnorePatterns": [ + "/dist/" + ] + }, + "gitHead": "3905ed045508b861b495f8d5630d76c7a157d8f1", + "dependencies": { + "@babel/runtime-corejs3": "7.21.0", + "boxen": "5.1.2", + "chalk": "4.1.2", + "core-js": "3.29.1", + "enquirer": "2.3.6", + "stdout-update": "1.6.8" + }, + "devDependencies": { + "@babel/cli": "7.21.0", + "@babel/core": "7.21.3", + "jest": "29.5.0", + "typescript": "5.0.2" + } +} diff --git a/packages/tui/src/__tests__/index.test.js b/packages/tui/src/__tests__/index.test.js new file mode 100644 index 000000000000..3b33f651c196 --- /dev/null +++ b/packages/tui/src/__tests__/index.test.js @@ -0,0 +1 @@ +test.todo('implement tests') diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts new file mode 100644 index 000000000000..b86a8354f1d3 --- /dev/null +++ b/packages/tui/src/index.ts @@ -0,0 +1,286 @@ +// + +import stream from 'stream' + +import boxen from 'boxen' +import chalk from 'chalk' +import { prompt as enquirerPrompt } from 'enquirer' +import { UpdateManager } from 'stdout-update' + +/** + * Specifications: + * - Prompting support + * - A basic progress bar + * - Templates for common tasks, printing errors etc. + */ + +export interface RedwoodTUIConfig { + out?: NodeJS.WriteStream + err?: NodeJS.WriteStream +} + +export interface RedwoodTUIHeaderOptions { + spinner: boolean + spinnerIndex: number + spinnerCharacters: string[] +} + +/** + * To keep a consistent color/style palette between cli packages, such as + * @redwood/create-redwood-app and @redwood/cli, please keep them compatible + * with one and another. We'll might split up and refactor these into a + * separate package when there is a strong motivation behind it. + * + * Current files: + * + * - packages/cli/src/lib/colors.js + * - packages/create-redwood-app/src/create-redwood-app.js (this file) + * + */ +export const styling = { + error: chalk.bold.red, + warning: chalk.keyword('orange'), + success: chalk.greenBright, + info: chalk.grey, + + header: chalk.bold.underline.hex('#e8e8e8'), + cmd: chalk.hex('#808080'), + redwood: chalk.hex('#ff845e'), + love: chalk.redBright, + + green: chalk.green, +} + +/** + * TODO: Documentation for this + */ +export class RedwoodTUI { + private manager: UpdateManager + + private timerId?: NodeJS.Timer + private looping = false + + private header?: string + private headerOptions = { + spinner: false, + spinnerIndex: 0, + spinnerCharacters: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], + } + + private boxen?: boxen.Options + + private contentText = '' + private contentMode: 'text' | 'stream' = 'text' + + private outStream?: stream.Writable + private errStream?: stream.Writable + + constructor({ out, err }: RedwoodTUIConfig = {}) { + this.manager = UpdateManager.getInstance(out, err) + } + + /** + * Enable control of the terminal and allow writing content + */ + enable() { + if (!this.manager.isHooked) { + this.manager.hook() + this.looping = true + this.timerId = setInterval(() => { + this.loop() + }, 80) + } + } + + /** + * Disable control of the terminal and stop writing content + */ + disable() { + if (this.manager.isHooked) { + // Stop the draw loop + this.looping = false + clearInterval(this.timerId) + + // Draw one last time + this.loop(true) + + this.outStream?.destroy() + this.errStream?.destroy() + this.manager.unhook() + } + } + + /** + * The draw loop + */ + loop(force = false) { + if (this.looping || force) { + const builtContent = this.buildContent() + const content: string[] = [] + if (this.boxen) { + content.push(...boxen(builtContent || '', this.boxen).split('\n')) + } else { + if (this.header) { + if (this.headerOptions.spinner) { + content.push( + `${ + this.headerOptions.spinnerCharacters[ + this.headerOptions.spinnerIndex + ] + } ${this.header}` + ) + this.headerOptions.spinnerIndex += 1 + this.headerOptions.spinnerIndex = + this.headerOptions.spinnerIndex % + this.headerOptions.spinnerCharacters.length + } else { + content.push(this.header) + } + } + if (builtContent) { + content.push(builtContent) + } + } + this.manager.update(content) + } + } + + /** + * TODO: This should be used to prevent overriding all of the history, like move on to a newline and update any new content + */ + moveOn() { + // If needed redraw to remove artifacts like spinners + if (this.header) { + this.setHeader(this.header, { spinner: false }) + this.loop(true) + } + + // Disable + this.disable() + + // Clear any previously defined content and settings + this.contentText = '' + this.contentMode = 'text' + this.clearHeader() + this.clearBoxen() + + // Re-enable + this.enable() + } + + buildContent(): string { + const content: string[] = [] + switch (this.contentMode) { + case 'text': + if (this.contentText) { + content.push(this.contentText) + } + break + case 'stream': + if (this.contentText) { + content.push(this.contentText) + } + break + } + return content.join('\n') + } + + // --- + + setHeader(header: string, options?: Partial) { + this.header = header + this.headerOptions = { ...this.headerOptions, ...options } + } + + clearHeader() { + this.header = undefined + } + + // --- + + setBoxen(options: boxen.Options) { + this.boxen = options + } + + clearBoxen() { + this.boxen = undefined + } + + // --- + + setContent(text: string) { + this.contentText = text + } + + setContentMode(mode: 'text' | 'stream') { + this.contentMode = mode + } + + // --- + + setOutStream(out: stream.Readable) { + this.outStream = new stream.Writable({ + write: (chunk: Buffer, _encoding, next) => { + if (this.contentMode === 'stream') { + this.contentText += chunk.toString('utf-8') + } + next() + return true + }, + }) + out.pipe(this.outStream, { end: true }) + } + + setErrStream(err: stream.Readable) { + this.errStream = new stream.Writable({ + write: (_chunk, _encoding, _next) => { + // if(this.contentMode === "stream"){ + // this.contentText += chunk + // } + return true + }, + }) + err.pipe(this.errStream, { end: true }) + } + + // --- + + drawLinesAndMoveOn(...lines: string[]) { + lines.forEach((line) => { + this.setContentMode('text') + this.setContent(line) + this.loop(true) + this.moveOn() + }) + } + + // --- + + promptA = enquirerPrompt + + // TODO: Fix types of questions, enquirer does not export their types... + async prompt(questions: any): Promise { + this.moveOn() + this.disable() + const result = await enquirerPrompt(questions) + this.enable() + return result + } + + // --- + + displayError(title: string, message: string) { + this.moveOn() + this.setBoxen({ + padding: 1, + borderColor: 'red', + title: `⚠ Error: ${title}`, + titleAlignment: 'left', + }) + this.setContentMode('text') + this.setContent(message) + this.moveOn() + } + + // --- +} diff --git a/packages/tui/tsconfig.json b/packages/tui/tsconfig.json new file mode 100644 index 000000000000..43947dd79bd1 --- /dev/null +++ b/packages/tui/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.compilerOption.json", + "compilerOptions": { + "baseUrl": ".", + "rootDir": "src", + "tsBuildInfoFile": "dist/tsconfig.tsbuildinfo", + "outDir": "dist" + }, + "include": ["src"], +} diff --git a/yarn.lock b/yarn.lock index 6c919dc7c665..31d111be489f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7129,6 +7129,23 @@ __metadata: languageName: unknown linkType: soft +"@redwoodjs/tui@workspace:packages/tui": + version: 0.0.0-use.local + resolution: "@redwoodjs/tui@workspace:packages/tui" + dependencies: + "@babel/cli": 7.21.0 + "@babel/core": 7.21.3 + "@babel/runtime-corejs3": 7.21.0 + boxen: 5.1.2 + chalk: 4.1.2 + core-js: 3.29.1 + enquirer: 2.3.6 + jest: 29.5.0 + stdout-update: 1.6.8 + typescript: 5.0.2 + languageName: unknown + linkType: soft + "@redwoodjs/vite@workspace:packages/vite": version: 0.0.0-use.local resolution: "@redwoodjs/vite@workspace:packages/vite" @@ -10887,7 +10904,7 @@ __metadata: languageName: node linkType: hard -"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0": +"ansi-escapes@npm:^4.2.1, ansi-escapes@npm:^4.3.0, ansi-escapes@npm:^4.3.2": version: 4.3.2 resolution: "ansi-escapes@npm:4.3.2" dependencies: @@ -10953,7 +10970,7 @@ __metadata: languageName: node linkType: hard -"ansi-styles@npm:^5.0.0": +"ansi-styles@npm:^5.0.0, ansi-styles@npm:^5.2.0": version: 5.2.0 resolution: "ansi-styles@npm:5.2.0" checksum: 9c4ca80eb3c2fb7b33841c210d2f20807f40865d27008d7c3f707b7f95cab7d67462a565e2388ac3285b71cb3d9bb2173de8da37c57692a362885ec34d6e27df @@ -14230,11 +14247,9 @@ __metadata: chalk: 4.1.2 check-node-version: 4.2.1 core-js: 3.29.1 - enquirer: 2.3.6 execa: 5.1.1 fs-extra: 11.1.1 jest: 29.5.0 - listr2: 5.0.8 terminal-link: 2.1.1 typescript: 5.0.2 yargs: 17.7.1 @@ -28815,6 +28830,18 @@ __metadata: languageName: node linkType: hard +"stdout-update@npm:1.6.8": + version: 1.6.8 + resolution: "stdout-update@npm:1.6.8" + dependencies: + ansi-escapes: ^4.3.2 + ansi-styles: ^5.2.0 + string-width: ^4.2.2 + strip-ansi: ^6.0.0 + checksum: 2435eba195367cde1bb4f4ae19af3f9a7d38f240bfa05c9f562c0feb8e6f46fd83f78e928b3025f105e6b3baefcca693009cc46b7065679ca0edaec2e223f193 + languageName: node + linkType: hard + "stop-iteration-iterator@npm:^1.0.0": version: 1.0.0 resolution: "stop-iteration-iterator@npm:1.0.0" From 8bae2821189218c2dcca3b0135497addde42b8a4 Mon Sep 17 00:00:00 2001 From: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Tue, 4 Apr 2023 15:01:12 +0100 Subject: [PATCH 02/10] Fix deps --- packages/tui/package.json | 2 +- yarn.lock | 368 ++------------------------------------ 2 files changed, 20 insertions(+), 350 deletions(-) diff --git a/packages/tui/package.json b/packages/tui/package.json index dad8f6925c67..e84fdad6e190 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -39,6 +39,6 @@ "@babel/cli": "7.21.0", "@babel/core": "7.21.3", "jest": "29.5.0", - "typescript": "5.0.2" + "typescript": "5.0.3" } } diff --git a/yarn.lock b/yarn.lock index e1e6dca24f46..ce7baf01cc65 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3984,16 +3984,6 @@ __metadata: languageName: node linkType: hard -"@grpc/grpc-js@npm:^1.7.1": - version: 1.8.13 - resolution: "@grpc/grpc-js@npm:1.8.13" - dependencies: - "@grpc/proto-loader": ^0.7.0 - "@types/node": ">=12.12.47" - checksum: 8cbba11451f094bcd347beadbb2234ebf2accbf16f0639e2618b2d874490c139f7100eeebe97f0ec1cdbdaa31c9e9e5242a7f5680625353b1f25af5f86be793d - languageName: node - linkType: hard - "@grpc/grpc-js@npm:~1.7.0": version: 1.7.3 resolution: "@grpc/grpc-js@npm:1.7.3" @@ -4044,21 +4034,6 @@ __metadata: languageName: node linkType: hard -"@grpc/proto-loader@npm:^0.7.3": - version: 0.7.6 - resolution: "@grpc/proto-loader@npm:0.7.6" - dependencies: - "@types/long": ^4.0.1 - lodash.camelcase: ^4.3.0 - long: ^4.0.0 - protobufjs: ^7.0.0 - yargs: ^16.2.0 - bin: - proto-loader-gen-types: build/bin/proto-loader-gen-types.js - checksum: 810d1116ecbb43f017f4f22890a1205642d9b633d79429f659fb870f3e7fdaba890b56f26b517334798d761b6ac873454c9c07c36493258bb71f0006b64e5125 - languageName: node - linkType: hard - "@hexagon/base64@npm:^1.1.25": version: 1.1.25 resolution: "@hexagon/base64@npm:1.1.25" @@ -5532,6 +5507,13 @@ __metadata: languageName: node linkType: hard +"@opentelemetry/api@npm:1.4.1": + version: 1.4.1 + resolution: "@opentelemetry/api@npm:1.4.1" + checksum: 5ee641d3d64c91e87ee328fc22251fc70c809a3c744e51e595ca77c0bd3cad933b77a79beb4dac66b811e5068941cef9da58c1ec217c0748a01f598e08a7ae66 + languageName: node + linkType: hard + "@opentelemetry/api@npm:^1.3.0": version: 1.4.0 resolution: "@opentelemetry/api@npm:1.4.0" @@ -5539,13 +5521,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.4.1": - version: 1.4.1 - resolution: "@opentelemetry/api@npm:1.4.1" - checksum: 5ee641d3d64c91e87ee328fc22251fc70c809a3c744e51e595ca77c0bd3cad933b77a79beb4dac66b811e5068941cef9da58c1ec217c0748a01f598e08a7ae66 - languageName: node - linkType: hard - "@opentelemetry/context-async-hooks@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/context-async-hooks@npm:1.11.0" @@ -5577,37 +5552,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-jaeger@npm:1.11.0": - version: 1.11.0 - resolution: "@opentelemetry/exporter-jaeger@npm:1.11.0" - dependencies: - "@opentelemetry/core": 1.11.0 - "@opentelemetry/sdk-trace-base": 1.11.0 - "@opentelemetry/semantic-conventions": 1.11.0 - jaeger-client: ^3.15.0 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 5849a70831078f85ff93253f94d4db23a67c88aa2d2b55338f501a83f47339847bf5d122523cc58e96109b74d08d2f8ba02d3831d6257862e2dd995e8490386b - languageName: node - linkType: hard - -"@opentelemetry/exporter-trace-otlp-grpc@npm:0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.37.0" - dependencies: - "@grpc/grpc-js": ^1.7.1 - "@opentelemetry/core": 1.11.0 - "@opentelemetry/otlp-grpc-exporter-base": 0.37.0 - "@opentelemetry/otlp-transformer": 0.37.0 - "@opentelemetry/resources": 1.11.0 - "@opentelemetry/sdk-trace-base": 1.11.0 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 49c2725afd5b5930719015322bae3192dbe9e36df2fff400cddba4c1de2cc48c08f5e1907bde085f22a99d8c34bfaacca7a87f6bb550f5fbcb4254868fba1caa - languageName: node - linkType: hard - -"@opentelemetry/exporter-trace-otlp-http@npm:0.37.0, @opentelemetry/exporter-trace-otlp-http@npm:^0.37.0": +"@opentelemetry/exporter-trace-otlp-http@npm:0.37.0": version: 0.37.0 resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.37.0" dependencies: @@ -5622,49 +5567,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-proto@npm:0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.37.0" - dependencies: - "@opentelemetry/core": 1.11.0 - "@opentelemetry/otlp-exporter-base": 0.37.0 - "@opentelemetry/otlp-proto-exporter-base": 0.37.0 - "@opentelemetry/otlp-transformer": 0.37.0 - "@opentelemetry/resources": 1.11.0 - "@opentelemetry/sdk-trace-base": 1.11.0 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 01079a37d7e1d8f0c624f4ce25544309f2d16f3ceb71f716af230edc783616a20231566063ebf36138d07e5c75375d949069272daecb501b951705cf087e5b46 - languageName: node - linkType: hard - -"@opentelemetry/exporter-zipkin@npm:1.11.0": - version: 1.11.0 - resolution: "@opentelemetry/exporter-zipkin@npm:1.11.0" - dependencies: - "@opentelemetry/core": 1.11.0 - "@opentelemetry/resources": 1.11.0 - "@opentelemetry/sdk-trace-base": 1.11.0 - "@opentelemetry/semantic-conventions": 1.11.0 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 2446d25d51066c4c6820f40273b07e4598b5835c2a800cfbb5454f62aa7a20c6770dfeafdddf4506a5c3ecf4a831e70777ed41596330cb02db9fef0668b4db8f - languageName: node - linkType: hard - -"@opentelemetry/instrumentation@npm:0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/instrumentation@npm:0.37.0" - dependencies: - require-in-the-middle: ^6.0.0 - semver: ^7.3.2 - shimmer: ^1.2.1 - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 2f0981b058dd86d671ed3b81e28eccf7642d44f4267277cb26ee0df943f2b09898a7ab8089bcbbec4991813b0c812e7217d2a58b50806d12c586079cf68c019a - languageName: node - linkType: hard - "@opentelemetry/otlp-exporter-base@npm:0.37.0": version: 0.37.0 resolution: "@opentelemetry/otlp-exporter-base@npm:0.37.0" @@ -5676,33 +5578,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/otlp-grpc-exporter-base@npm:0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.37.0" - dependencies: - "@grpc/grpc-js": ^1.7.1 - "@grpc/proto-loader": ^0.7.3 - "@opentelemetry/core": 1.11.0 - "@opentelemetry/otlp-exporter-base": 0.37.0 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 7ddc746a4c991a7a8930d593b54368454e3838616693a0cc4dc8958bbd7ab5317630a8b834c358d9b3361fcf143ef3070b0172ea9ebee3f8c1a2717b1b7f180d - languageName: node - linkType: hard - -"@opentelemetry/otlp-proto-exporter-base@npm:0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/otlp-proto-exporter-base@npm:0.37.0" - dependencies: - "@opentelemetry/core": 1.11.0 - "@opentelemetry/otlp-exporter-base": 0.37.0 - protobufjs: ^7.1.2 - peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 61f2dcbb1532ad36d2bf19f23c7cebc07ad7f7c3c92f2cb8e611a6cdb55af730a0779008ef022221bfca816334f0bd1aef38fe65f73c2b6485ec532039fd7279 - languageName: node - linkType: hard - "@opentelemetry/otlp-transformer@npm:0.37.0": version: 0.37.0 resolution: "@opentelemetry/otlp-transformer@npm:0.37.0" @@ -5739,7 +5614,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resources@npm:1.11.0, @opentelemetry/resources@npm:^1.11.0": +"@opentelemetry/resources@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/resources@npm:1.11.0" dependencies: @@ -5776,28 +5651,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-node@npm:^0.37.0": - version: 0.37.0 - resolution: "@opentelemetry/sdk-node@npm:0.37.0" - dependencies: - "@opentelemetry/core": 1.11.0 - "@opentelemetry/exporter-jaeger": 1.11.0 - "@opentelemetry/exporter-trace-otlp-grpc": 0.37.0 - "@opentelemetry/exporter-trace-otlp-http": 0.37.0 - "@opentelemetry/exporter-trace-otlp-proto": 0.37.0 - "@opentelemetry/exporter-zipkin": 1.11.0 - "@opentelemetry/instrumentation": 0.37.0 - "@opentelemetry/resources": 1.11.0 - "@opentelemetry/sdk-metrics": 1.11.0 - "@opentelemetry/sdk-trace-base": 1.11.0 - "@opentelemetry/sdk-trace-node": 1.11.0 - "@opentelemetry/semantic-conventions": 1.11.0 - peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.5.0" - checksum: ec159b2d60bf82075eb8fb7639c6a56b382fe0a8e7ddb9c1c0dd6ad632d1d746a1c9068e4ac9e834d23a98b83a9a8fe1f190035e644b9f3c3148b60ae409312e - languageName: node - linkType: hard - "@opentelemetry/sdk-trace-base@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/sdk-trace-base@npm:1.11.0" @@ -5824,7 +5677,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-node@npm:1.11.0, @opentelemetry/sdk-trace-node@npm:^1.11.0": +"@opentelemetry/sdk-trace-node@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/sdk-trace-node@npm:1.11.0" dependencies: @@ -5840,7 +5693,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.11.0, @opentelemetry/semantic-conventions@npm:^1.11.0": +"@opentelemetry/semantic-conventions@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/semantic-conventions@npm:1.11.0" checksum: 315c8074e01539fd5990f37dd2fdb14ca6189706634d60e0be06e9d84d66c7c68adb9522c8d0d61b638c4dcd4065f8b011017b68d09bd8da23ee7afba6097f74 @@ -7439,7 +7292,7 @@ __metadata: enquirer: 2.3.6 jest: 29.5.0 stdout-update: 1.6.8 - typescript: 5.0.2 + typescript: 5.0.3 languageName: unknown linkType: soft @@ -11174,13 +11027,6 @@ __metadata: languageName: node linkType: hard -"ansi-color@npm:^0.2.1": - version: 0.2.1 - resolution: "ansi-color@npm:0.2.1" - checksum: 0ccfb57aadd3a955d1bbdc8392fb24e7e13b247812955b0182a3bea3f733d757e49f0b8faa00a6d2c619613f2d2a3136096f91e1fb4d688c39cf24b4f776a0d3 - languageName: node - linkType: hard - "ansi-colors@npm:4.1.3, ansi-colors@npm:^4.1.1": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -12810,18 +12656,6 @@ __metadata: languageName: node linkType: hard -"bufrw@npm:^1.3.0": - version: 1.3.0 - resolution: "bufrw@npm:1.3.0" - dependencies: - ansi-color: ^0.2.1 - error: ^7.0.0 - hexer: ^1.5.0 - xtend: ^4.0.0 - checksum: 607de41603db6c5e92c270ac1020f3b0c25d61e33fb9bcd6149d7cb927022b5530464db2c491050571404759ed0047a01db40467a58b99dadee4c6d4506df6c3 - languageName: node - linkType: hard - "builtin-status-codes@npm:^3.0.0": version: 3.0.0 resolution: "builtin-status-codes@npm:3.0.0" @@ -14554,17 +14388,12 @@ __metadata: resolution: "create-redwood-app@workspace:packages/create-redwood-app" dependencies: "@babel/cli": 7.21.0 - "@babel/core": 7.21.3 - "@babel/node": 7.20.7 "@babel/runtime-corejs3": 7.21.0 - "@opentelemetry/api": ^1.4.1 - "@opentelemetry/exporter-trace-otlp-http": ^0.37.0 - "@opentelemetry/resources": ^1.11.0 - "@opentelemetry/sdk-node": ^0.37.0 - "@opentelemetry/sdk-trace-node": ^1.11.0 - "@opentelemetry/semantic-conventions": ^1.11.0 - "@redwoodjs/project-config": 4.0.0 - "@redwoodjs/telemetry": 4.0.0 + "@opentelemetry/api": 1.4.1 + "@opentelemetry/exporter-trace-otlp-http": 0.37.0 + "@opentelemetry/resources": 1.11.0 + "@opentelemetry/sdk-trace-node": 1.11.0 + "@opentelemetry/semantic-conventions": 1.11.0 "@types/prompts": 2.4.4 chalk: 4.1.2 check-node-version: 4.2.1 @@ -16143,25 +15972,6 @@ __metadata: languageName: node linkType: hard -"error@npm:7.0.2": - version: 7.0.2 - resolution: "error@npm:7.0.2" - dependencies: - string-template: ~0.2.1 - xtend: ~4.0.0 - checksum: abee95f258f34490278bfb5f5852420e23f9d7dd7754215144391a731c2e7f68ccb5367497ca7cc20459d1eb7ae5d119d6c82f620a9340150034ddd2e3603178 - languageName: node - linkType: hard - -"error@npm:^7.0.0": - version: 7.2.1 - resolution: "error@npm:7.2.1" - dependencies: - string-template: ~0.2.1 - checksum: 91ce301017292eab20b59e27a0bc322a8f45fcf48d992761530d20c5f9c5699a2ae1822fc94298d4815fd35c2595e89139a7c6fdd3bbe9e93871e3b412186567 - languageName: node - linkType: hard - "es-abstract@npm:^1.19.0, es-abstract@npm:^1.20.4": version: 1.21.1 resolution: "es-abstract@npm:1.21.1" @@ -19164,20 +18974,6 @@ __metadata: languageName: node linkType: hard -"hexer@npm:^1.5.0": - version: 1.5.0 - resolution: "hexer@npm:1.5.0" - dependencies: - ansi-color: ^0.2.1 - minimist: ^1.1.0 - process: ^0.10.0 - xtend: ^4.0.0 - bin: - hexer: ./cli.js - checksum: 43b00fad220a98ed98dad3a3d0e7297bd3d0ce66f0d935a2927e07c0d29f5b8de92f9c0fa32c641daa4291ccb19385fa1bd0853d298983ec9b3ec88e7686ee5c - languageName: node - linkType: hard - "hmac-drbg@npm:^1.0.1": version: 1.0.1 resolution: "hmac-drbg@npm:1.0.1" @@ -20780,19 +20576,6 @@ __metadata: languageName: node linkType: hard -"jaeger-client@npm:^3.15.0": - version: 3.19.0 - resolution: "jaeger-client@npm:3.19.0" - dependencies: - node-int64: ^0.4.0 - opentracing: ^0.14.4 - thriftrw: ^3.5.0 - uuid: ^8.3.2 - xorshift: ^1.1.1 - checksum: 04f5683461212de49e4d5b6ca6b214276a797e361fba852231bc5e7fdcee76b053a6e618e5490106d7f7254917d928dda0880f7cd71e35dece7e09bbdcdd0927 - languageName: node - linkType: hard - "jake@npm:^10.8.5": version: 10.8.5 resolution: "jake@npm:10.8.5" @@ -22611,13 +22394,6 @@ __metadata: languageName: node linkType: hard -"long@npm:^2.4.0": - version: 2.4.0 - resolution: "long@npm:2.4.0" - checksum: 2560d6a299f2177b94ce300cc21fc15dbf72d9899ddac7bd10251468e1b4eabbce48463f651fea15cc4acf2c4775e3507767c58df73945d5d6ae4e12ac058796 - languageName: node - linkType: hard - "long@npm:^4.0.0": version: 4.0.0 resolution: "long@npm:4.0.0" @@ -23422,7 +23198,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.1.0, minimist@npm:^1.1.1, minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:^1.1.1, minimist@npm:^1.1.3, minimist@npm:^1.2.0, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -23597,13 +23373,6 @@ __metadata: languageName: node linkType: hard -"module-details-from-path@npm:^1.0.3": - version: 1.0.3 - resolution: "module-details-from-path@npm:1.0.3" - checksum: 3d881f3410c142e4c2b1307835a2862ba04e5b3ec6e90655614a0ee2c4b299b4c1d117fb525d2435bf436990026f18d338a197b54ad6bd36252f465c336ff423 - languageName: node - linkType: hard - "module-not-found-error@npm:^1.0.1": version: 1.0.1 resolution: "module-not-found-error@npm:1.0.1" @@ -24792,13 +24561,6 @@ __metadata: languageName: node linkType: hard -"opentracing@npm:^0.14.4": - version: 0.14.7 - resolution: "opentracing@npm:0.14.7" - checksum: a7be8d697b1997548233423f5f4c196e285af8e864a24d7704fc6029beb73cd1f987651ca814e207629c6bc624cb03297a86601c0dc51cdca9a07a20f97b71ea - languageName: node - linkType: hard - "optimism@npm:^0.16.1": version: 0.16.2 resolution: "optimism@npm:0.16.2" @@ -26397,13 +26159,6 @@ __metadata: languageName: node linkType: hard -"process@npm:^0.10.0": - version: 0.10.1 - resolution: "process@npm:0.10.1" - checksum: 2608c672bb59fbd5e87f0c4df512540995af7e047c4eed024e1a4a1c9b144ce394840ce15069a97b82afef7aa58c6e2e8e959c1b711fe3a1f7bfb6529b03084c - languageName: node - linkType: hard - "process@npm:^0.11.10": version: 0.11.10 resolution: "process@npm:0.11.10" @@ -26605,26 +26360,6 @@ __metadata: languageName: node linkType: hard -"protobufjs@npm:^7.1.2": - version: 7.2.3 - resolution: "protobufjs@npm:7.2.3" - dependencies: - "@protobufjs/aspromise": ^1.1.2 - "@protobufjs/base64": ^1.1.2 - "@protobufjs/codegen": ^2.0.4 - "@protobufjs/eventemitter": ^1.1.0 - "@protobufjs/fetch": ^1.1.0 - "@protobufjs/float": ^1.0.2 - "@protobufjs/inquire": ^1.1.0 - "@protobufjs/path": ^1.1.2 - "@protobufjs/pool": ^1.1.0 - "@protobufjs/utf8": ^1.1.0 - "@types/node": ">=13.7.0" - long: ^5.0.0 - checksum: 8092a67a31d961622322887a2374a63a3509f22d45ed7ada39be5987d590bf3ec46eb17aa64ba08f79b42be44adfce63c2fdedc389aea1423013417b6af2c61f - languageName: node - linkType: hard - "protocols@npm:^2.0.0, protocols@npm:^2.0.1": version: 2.0.1 resolution: "protocols@npm:2.0.1" @@ -27750,17 +27485,6 @@ __metadata: languageName: node linkType: hard -"require-in-the-middle@npm:^6.0.0": - version: 6.0.0 - resolution: "require-in-the-middle@npm:6.0.0" - dependencies: - debug: ^4.1.1 - module-details-from-path: ^1.0.3 - resolve: ^1.22.1 - checksum: 8d44c4794b318eb393645e303cb336cb4baf3e9d2e368dac0a4df1a59219c970e17a52c389bf02e22fde33af6bb3206552eb388b9edf082972037c6169ec2eea - languageName: node - linkType: hard - "require-main-filename@npm:^2.0.0": version: 2.0.0 resolution: "require-main-filename@npm:2.0.0" @@ -28674,13 +28398,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: ae8b27c389db2a00acfc8da90240f11577685a8f3e40008f826a3bea8b4f3b3ecd305c26be024b4a0fd3b123d132c1569d6e238097960a9a543b6c60760fb46a - languageName: node - linkType: hard - "side-channel@npm:^1.0.4": version: 1.0.4 resolution: "side-channel@npm:1.0.4" @@ -29385,13 +29102,6 @@ __metadata: languageName: node linkType: hard -"string-template@npm:~0.2.1": - version: 0.2.1 - resolution: "string-template@npm:0.2.1" - checksum: 5dc9bd8741e50aaf1ebb616c64fdada32301dc52718692a7a13088285b96fecd1010ab612b348ef29c08dff4df4f96c8e80689ca855a578d01cc182e48199182 - languageName: node - linkType: hard - "string-width@npm:4.2.3, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -30129,19 +29839,6 @@ __metadata: languageName: node linkType: hard -"thriftrw@npm:^3.5.0": - version: 3.12.0 - resolution: "thriftrw@npm:3.12.0" - dependencies: - bufrw: ^1.3.0 - error: 7.0.2 - long: ^2.4.0 - bin: - thrift2json: ./thrift2json.js - checksum: 3f7f4184eb3d722c8f07be7a27f960f9ebe4c014532cb77d625b69f2246e5c3f9f1e7be2dbbf438372e6d6827b32ba55f658c80addcb9b511e907017205f9213 - languageName: node - linkType: hard - "throttle-debounce@npm:^3.0.1": version: 3.0.1 resolution: "throttle-debounce@npm:3.0.1" @@ -30802,16 +30499,6 @@ __metadata: languageName: node linkType: hard -"typescript@npm:5.0.2": - version: 5.0.2 - resolution: "typescript@npm:5.0.2" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: aa0e2a766412a0c9f609133681753cc88098aa44e079820d203dc0628a8fc3997d4e6b93c2c813c483f770698561c3225c5ab3e5c8896639cfc5d94528e3005f - languageName: node - linkType: hard - "typescript@npm:5.0.3": version: 5.0.3 resolution: "typescript@npm:5.0.3" @@ -30832,16 +30519,6 @@ __metadata: languageName: node linkType: hard -"typescript@patch:typescript@5.0.2#~builtin": - version: 5.0.2 - resolution: "typescript@patch:typescript@npm%3A5.0.2#~builtin::version=5.0.2&hash=85af82" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 3ff588d800f76d5f46b7ee41ca713d9c4274ea10f3283aa8bb2cec834075622e23a743eb63b8734da4ff2780f946849a009a8571ff59ac297abe4011125361fe - languageName: node - linkType: hard - "typescript@patch:typescript@5.0.3#~builtin": version: 5.0.3 resolution: "typescript@patch:typescript@npm%3A5.0.3#~builtin::version=5.0.3&hash=85af82" @@ -32624,13 +32301,6 @@ __metadata: languageName: node linkType: hard -"xorshift@npm:^1.1.1": - version: 1.2.0 - resolution: "xorshift@npm:1.2.0" - checksum: e805cdda3ca16ea48d3e1dbcb6f55f7c135bcf5219ae842bdea814486e4e6788cefc6703a04140b93be631370c1a7403c51ab0a6554ec68d66e84b601e34a722 - languageName: node - linkType: hard - "xregexp@npm:4.0.0": version: 4.0.0 resolution: "xregexp@npm:4.0.0" @@ -32638,7 +32308,7 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.0, xtend@npm:~4.0.1": +"xtend@npm:^4.0.0, xtend@npm:^4.0.1, xtend@npm:~4.0.1": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: 366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e From f41eac17915cee5d6ac68e0c5a2f143450bbc1e3 Mon Sep 17 00:00:00 2001 From: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:06:03 +0100 Subject: [PATCH 03/10] Rewrite tui package --- packages/create-redwood-app/package.json | 1 + .../src/create-redwood-app.js | 426 ++++++++++------ packages/tui/src/index.ts | 478 ++++++++++-------- yarn.lock | 3 +- 4 files changed, 546 insertions(+), 362 deletions(-) diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index 80067ef29338..d6d846c7b9d2 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -27,6 +27,7 @@ "@opentelemetry/resources": "1.11.0", "@opentelemetry/sdk-trace-node": "1.11.0", "@opentelemetry/semantic-conventions": "1.11.0", + "@redwoodjs/tui": "4.0.0", "chalk": "4.1.2", "check-node-version": "4.2.1", "ci-info": "3.8.0", diff --git a/packages/create-redwood-app/src/create-redwood-app.js b/packages/create-redwood-app/src/create-redwood-app.js index bb7995f45476..de95d424f47b 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -18,7 +18,7 @@ import terminalLink from 'terminal-link' import { hideBin, Parser } from 'yargs/helpers' import yargs from 'yargs/yargs' -import { RedwoodTUI, styling } from '@redwoodjs/tui' +import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@redwoodjs/tui' import { name, version } from '../package' @@ -33,17 +33,26 @@ const { telemetry } = Parser(hideBin(process.argv)) const tui = new RedwoodTUI() -async function checkCompatibility(templateDir, yarnInstall) { - tui.setContentMode('text') - tui.setHeader('Checking node and yarn compatibility', { spinner: true }) +async function executeCompatibilityCheck(templateDir, yarnInstall) { + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Checking node and yarn compatibility', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) if (!yarnInstall) { - tui.setContent( - ` ${styling.warning( - '⚠ Warning' - )}: Skipped check because yarn install was skipped with "--yarn-install false"` - ) - tui.moveOn() + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.warning( + '⚠' + )} Skipped compatibility check because yarn install was skipped via command line flag`, + }) + tui.stopReactive() return } @@ -68,13 +77,18 @@ async function checkCompatibility(templateDir, yarnInstall) { ) if (engineCheckPassed) { - tui.setContent(' ✔ Compatibility checks passed') - tui.moveOn() + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.green('✔')} Compatibility checks passed`, + }) + tui.stopReactive() return } - // TODO: Handle engine check errors if (!engineCheckPassed) { + tui.stopReactive(true) const engineCheckErrorDocs = terminalLink( 'Tutorial - Prerequisites', 'https://redwoodjs.com/docs/tutorial/chapter1/prerequisites' @@ -109,65 +123,31 @@ async function checkCompatibility(templateDir, yarnInstall) { } } -async function promptForTypescript() { - tui.setContentMode('text') - try { - const response = await tui.prompt({ - type: 'Select', - name: 'language', - choices: ['TypeScript', 'JavaScript'], - message: 'Select your preferred coding language', - initial: 'TypeScript', - }) - return response.language === 'TypeScript' - } catch (_error) { - recordErrorViaTelemetry('User cancelled install at language prompt') - await shutdownTelemetry() - process.exit(1) - } -} - -async function promptForGit() { - tui.setContentMode('text') - try { - const response = await tui.prompt({ - type: 'Toggle', - name: 'git', - message: 'Do you want to initialize a git repo?', - enabled: 'Yes', - disabled: 'no', - initial: 'Yes', - }) - return response.git - } catch (_error) { - recordErrorViaTelemetry('User cancelled install at git prompt') - await shutdownTelemetry() - process.exit(1) - } -} - async function createProjectFiles(newAppDir, overwrite, yarn1) { + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Creating project files', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) + // Check if the new app directory already exists if (fs.existsSync(newAppDir) && !overwrite) { // Check if the directory contains files and show an error if it does if (fs.readdirSync(newAppDir).length > 0) { - tui.setBoxen({ - padding: 1, - borderColor: 'red', - title: '⚠ Project directory already contains files', - titleAlignment: 'left', - }) - tui.setContentMode('text') - tui.setContent( + tui.stopReactive(true) + tui.displayError( + 'Project directory already contains files', [ - `'${styling.info(newAppDir)}' already exists and is not empty`, + `'${RedwoodStyling.info(newAppDir)}' already exists and is not empty`, ``, - `You can use the '${styling.info( + `You can use the '${RedwoodStyling.info( 'overwrite' )}' flag to create the project even if target directory isn't empty`, ].join('\n') ) - tui.disable() recordErrorViaTelemetry(`Project directory already contains files`) await shutdownTelemetry() process.exit(1) @@ -212,12 +192,26 @@ async function createProjectFiles(newAppDir, overwrite, yarn1) { spaces: 2, }) } + + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.green('✔')} Project files created`, + }) + tui.stopReactive() } async function installNodeModules(newAppDir) { - tui.setContentMode('text') - tui.setHeader('Installing node modules', { spinner: true }) - tui.setContent(' ⏱ This could take a minute or more...') + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + header: 'Installing node modules', + content: ' ⏱ This could take a while...', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) const yarnInstallSubprocess = execa('yarn install', { shell: true, @@ -225,15 +219,13 @@ async function installNodeModules(newAppDir) { }) try { - // TODO: We didn't want to have the yarn output show up? - // tui.setOutStream(yarnInstallSubprocess.stdout) - // tui.setErrStream(yarnInstallSubprocess.stderr) await yarnInstallSubprocess } catch (error) { + tui.stopReactive(true) tui.displayError( "Couldn't install node modules", [ - `We could not install node modules via ${styling.info( + `We could not install node modules via ${RedwoodStyling.info( "'yarn install'" )}. Please see below for the full error message.`, '', @@ -245,14 +237,25 @@ async function installNodeModules(newAppDir) { process.exit(1) } - tui.setHeader('Node modules successfully installed', { spinner: false }) - tui.setContent('') - tui.moveOn() + tuiContent.update({ + header: '', + content: `${RedwoodStyling.green('✔')} Node modules successfully installed`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() } async function convertToJavascript(newAppDir) { - tui.setContentMode('text') - tui.setHeader('Converting TypeScript files to JavaScript', { spinner: true }) + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Converting TypeScript files to JavaScript', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) const conversionSubprocess = execa('yarn rw ts-to-js', { shell: true, @@ -260,15 +263,13 @@ async function convertToJavascript(newAppDir) { }) try { - // TODO: We didn't want to have the yarn output show up? - // tui.setOutStream(conversionSubprocess.stdout) - // tui.setErrStream(conversionSubprocess.stderr) await conversionSubprocess } catch (error) { + tui.stopReactive(true) tui.displayError( "Couldn't convert TypeScript files to JavaScript", [ - `We could not convert the Typescript files to Javascript using ${styling.info( + `We could not convert the Typescript files to Javascript using ${RedwoodStyling.info( "'yarn rw ts-to-js'" )}. Please see below for the full error message.`, '', @@ -280,13 +281,26 @@ async function convertToJavascript(newAppDir) { process.exit(1) } - tui.setHeader('Converted TypeScript files to JavaScript', { spinner: false }) - tui.moveOn() + tuiContent.update({ + content: `${RedwoodStyling.green( + '✔' + )} Converted TypeScript files to JavaScript`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() } async function generateTypes(newAppDir) { - tui.setContentMode('text') - tui.setHeader('Generating types', { spinner: true }) + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Generating types', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) const generateSubprocess = execa('yarn rw-gen', { shell: true, @@ -294,15 +308,13 @@ async function generateTypes(newAppDir) { }) try { - // TODO: We didn't want to have the yarn output show up? - // tui.setOutStream(conversionSubprocess.stdout) - // tui.setErrStream(conversionSubprocess.stderr) await generateSubprocess } catch (error) { + tui.stopReactive(true) tui.displayError( "Couldn't generate types", [ - `We could not generate types using ${styling.info( + `We could not generate types using ${RedwoodStyling.info( "'yarn rw-gen'" )}. Please see below for the full error message.`, '', @@ -314,13 +326,24 @@ async function generateTypes(newAppDir) { process.exit(1) } - tui.setHeader('Generated types', { spinner: false }) - tui.moveOn() + tuiContent.update({ + content: `${RedwoodStyling.green('✔')} Generated types`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() } async function initialiseGit(newAppDir) { - tui.setContentMode('text') - tui.setHeader('Initialising a git repo', { spinner: true }) + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Initialising a git repo', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) const gitSubprocess = execa( 'git init && git add . && git commit -m "Initial commit"', @@ -331,15 +354,13 @@ async function initialiseGit(newAppDir) { ) try { - // TODO: We didn't want to have the yarn output show up? - // tui.setOutStream(conversionSubprocess.stdout) - // tui.setErrStream(conversionSubprocess.stderr) await gitSubprocess } catch (error) { + tui.stopReactive(true) tui.displayError( "Couldn't initialise a git repo", [ - `We could not initialise a git repo using ${styling.info( + `We could not initialise a git repo using ${RedwoodStyling.info( 'git init && git add . && git commit -m "Initial commit"' )}. Please see below for the full error message.`, '', @@ -351,8 +372,70 @@ async function initialiseGit(newAppDir) { process.exit(1) } - tui.setHeader('Initialied a git repo', { spinner: false }) - tui.moveOn() + tuiContent.update({ + content: `${RedwoodStyling.green('✔')} Initialised a git repo`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() +} + +async function handleTypescriptPreference(typescriptFlag) { + // Handle case where flag is set + if (typescriptFlag !== null) { + tui.drawText( + `${RedwoodStyling.green('✔')} Using ${ + typescriptFlag ? 'TypeScript' : 'JavaScript' + } based on command line flag` + ) + return typescriptFlag + } + + // Prompt user for preference + try { + const response = await tui.prompt({ + type: 'Select', + name: 'language', + choices: ['TypeScript', 'JavaScript'], + message: 'Select your preferred coding language', + initial: 'TypeScript', + }) + return response.language === 'TypeScript' + } catch (_error) { + recordErrorViaTelemetry('User cancelled install at language prompt') + await shutdownTelemetry() + process.exit(1) + } +} + +async function handleGitPreference(gitInitFlag) { + // Handle case where flag is set + if (gitInitFlag !== null) { + tui.drawText( + `${RedwoodStyling.green('✔')} ${ + gitInitFlag ? 'Will' : 'Will not' + } initialise a git repo based on command line flag` + ) + return gitInitFlag + } + + // Prompt user for preference + try { + const response = await tui.prompt({ + type: 'Toggle', + name: 'git', + message: 'Do you want to initialize a git repo?', + enabled: 'Yes', + disabled: 'no', + initial: 'Yes', + }) + return response.git + } catch (_error) { + recordErrorViaTelemetry('User cancelled install at git prompt') + await shutdownTelemetry() + process.exit(1) + } } /** @@ -362,24 +445,27 @@ async function initialiseGit(newAppDir) { * - TODO - Add a list of what this function does */ async function createRedwoodApp() { - tui.enable() - // Introductory message - tui.drawLinesAndMoveOn( - `${styling.redwood('-'.repeat(66))}`, - `${' '.repeat(16)}🌲⚡️ ${styling.header('Welcome to RedwoodJS!')} ⚡️🌲`, - `${styling.redwood('-'.repeat(66))}` + tui.drawText( + [ + `${RedwoodStyling.redwood('-'.repeat(66))}`, + `${' '.repeat(16)}🌲⚡️ ${RedwoodStyling.header( + 'Welcome to RedwoodJS!' + )} ⚡️🌲`, + `${RedwoodStyling.redwood('-'.repeat(66))}`, + ].join('\n') ) // Extract the args as provided by the user in the command line + // TODO: Make all flags have the 'flag' suffix const { _: args, 'yarn-install': yarnInstall, - typescript, + typescript: typescriptFlag, overwrite, // telemetry, // Extracted above to check if telemetry is disabled before we even reach this point yarn1, - 'git-init': gitInit, + 'git-init': gitInitFlag, } = yargs(hideBin(process.argv)) .scriptName(name) .usage('Usage: $0 [option]') @@ -445,7 +531,6 @@ async function createRedwoodApp() { )}`, ].join('\n') ) - recordErrorViaTelemetry('No target directory specified') await shutdownTelemetry() process.exit(1) @@ -455,15 +540,14 @@ async function createRedwoodApp() { const templateDir = path.resolve(__dirname, '../template') // Engine check - await checkCompatibility(templateDir, yarnInstall) + await executeCompatibilityCheck(templateDir, yarnInstall) // Determine ts/js preference - const useTypescript = - typescript === null ? await promptForTypescript() : typescript + const useTypescript = await handleTypescriptPreference(typescriptFlag) trace.getActiveSpan().setAttribute('typescript', useTypescript) // Determine git preference - const useGit = gitInit === null ? await promptForGit() : gitInit + const useGit = await handleGitPreference(gitInitFlag) trace.getActiveSpan().setAttribute('git', useGit) // Create project files @@ -477,14 +561,28 @@ async function createRedwoodApp() { .getActiveSpan() .setAttribute('yarn-install-time', Date.now() - yarnInstallStart) } else { - tui.setContentMode('text') - tui.setContent('Skipping yarn install') - trace.getActiveSpan().setAttribute('yarn-install-time', 0) + tui.drawText( + `${RedwoodStyling.warning( + '⚠' + )} Skipped yarn install step based on command line flag` + ) } // Conditionally convert to javascript - if (!useTypescript && yarnInstall) { - await convertToJavascript(newAppDir) + if (!useTypescript) { + if (yarnInstall) { + await convertToJavascript(newAppDir) + } else { + tui.drawText( + [ + `${RedwoodStyling.warning( + '⚠' + )} Unable to convert to javascript without yarn install step`, + ' Please run the following command inside your project once yarn install has been executed:', + ` ${RedwoodStyling.info("'yarn rw ts-to-js'")}`, + ].join('\n') + ) + } } // Generate types @@ -498,54 +596,62 @@ async function createRedwoodApp() { } // Post install message - tui.drawLinesAndMoveOn( - '', - styling.success('Thanks for trying out Redwood!'), - '', - ` ⚡️ ${styling.redwood( - 'Get up and running fast with this Quick Start guide' - )}: https://redwoodjs.com/docs/quick-start`, - '', - styling.header('Join the Community'), - '', - `${styling.redwood(' ❖ Join our Forums')}: https://community.redwoodjs.com`, - `${styling.redwood(' ❖ Join our Chat')}: https://discord.gg/redwoodjs`, - '', - styling.header('Get some help'), - '', - `${styling.redwood( - ' ❖ Get started with the Tutorial' - )}: https://redwoodjs.com/docs/tutorial`, - `${styling.redwood( - ' ❖ Read the Documentation' - )}: https://redwoodjs.com/docs`, - '', - styling.header('Stay updated'), - '', - `${styling.redwood( - ' ❖ Sign up for our Newsletter' - )}: https://www.redwoodjs.com/newsletter`, - `${styling.redwood( - ' ❖ Follow us on Twitter' - )}: https://twitter.com/redwoodjs`, - '', - `${styling.header(`Become a Contributor`)} ${styling.love('❤')}`, - '', - `${styling.redwood( - ' ❖ Learn how to get started' - )}: https://redwoodjs.com/docs/contributing`, - `${styling.redwood( - ' ❖ Find a Good First Issue' - )}: https://redwoodjs.com/good-first-issue`, - '', - `${styling.header(`Fire it up!`)} 🚀`, - '', - `${styling.redwood(` > ${styling.green(`cd ${targetDir}`)}`)}`, - `${styling.redwood(` > ${styling.green(`yarn rw dev`)}`)}`, - '' + tui.drawText( + [ + '', + RedwoodStyling.success('Thanks for trying out Redwood!'), + '', + ` ⚡️ ${RedwoodStyling.redwood( + 'Get up and running fast with this Quick Start guide' + )}: https://redwoodjs.com/docs/quick-start`, + '', + RedwoodStyling.header('Join the Community'), + '', + `${RedwoodStyling.redwood( + ' ❖ Join our Forums' + )}: https://community.redwoodjs.com`, + `${RedwoodStyling.redwood( + ' ❖ Join our Chat' + )}: https://discord.gg/redwoodjs`, + '', + RedwoodStyling.header('Get some help'), + '', + `${RedwoodStyling.redwood( + ' ❖ Get started with the Tutorial' + )}: https://redwoodjs.com/docs/tutorial`, + `${RedwoodStyling.redwood( + ' ❖ Read the Documentation' + )}: https://redwoodjs.com/docs`, + '', + RedwoodStyling.header('Stay updated'), + '', + `${RedwoodStyling.redwood( + ' ❖ Sign up for our Newsletter' + )}: https://www.redwoodjs.com/newsletter`, + `${RedwoodStyling.redwood( + ' ❖ Follow us on Twitter' + )}: https://twitter.com/redwoodjs`, + '', + `${RedwoodStyling.header(`Become a Contributor`)} ${RedwoodStyling.love( + '❤' + )}`, + '', + `${RedwoodStyling.redwood( + ' ❖ Learn how to get started' + )}: https://redwoodjs.com/docs/contributing`, + `${RedwoodStyling.redwood( + ' ❖ Find a Good First Issue' + )}: https://redwoodjs.com/good-first-issue`, + '', + `${RedwoodStyling.header(`Fire it up!`)} 🚀`, + '', + `${RedwoodStyling.redwood( + ` > ${RedwoodStyling.green(`cd ${targetDir}`)}` + )}`, + `${RedwoodStyling.redwood(` > ${RedwoodStyling.green(`yarn rw dev`)}`)}`, + '', + ].join('\n') ) - - tui.disable() } ;(async () => { diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index b86a8354f1d3..3a397bd99467 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -9,16 +9,14 @@ import { UpdateManager } from 'stdout-update' /** * Specifications: - * - Prompting support * - A basic progress bar - * - Templates for common tasks, printing errors etc. + * - Templates for common tasks: + * - ~~Errors~~ + * - Warnings + * - Info + * - Success */ -export interface RedwoodTUIConfig { - out?: NodeJS.WriteStream - err?: NodeJS.WriteStream -} - export interface RedwoodTUIHeaderOptions { spinner: boolean spinnerIndex: number @@ -26,18 +24,20 @@ export interface RedwoodTUIHeaderOptions { } /** - * To keep a consistent color/style palette between cli packages, such as - * @redwood/create-redwood-app and @redwood/cli, please keep them compatible - * with one and another. We'll might split up and refactor these into a - * separate package when there is a strong motivation behind it. - * - * Current files: - * - * - packages/cli/src/lib/colors.js - * - packages/create-redwood-app/src/create-redwood-app.js (this file) + * Configuration for the TUI * + * Accepts an out and err stream which the TUI will write to. */ -export const styling = { +export interface RedwoodTUIConfig { + out?: NodeJS.WriteStream + err?: NodeJS.WriteStream +} + +// TODO: Rename this to RedwoodChalk? +/** + * A default set of styling for the TUI, designed for a cohesive look and feel around the Redwood CLI, CRWA and vairous plugins + */ +export const RedwoodStyling = { error: chalk.bold.red, warning: chalk.keyword('orange'), success: chalk.greenBright, @@ -51,236 +51,312 @@ export const styling = { green: chalk.green, } -/** - * TODO: Documentation for this - */ -export class RedwoodTUI { - private manager: UpdateManager - - private timerId?: NodeJS.Timer - private looping = false - - private header?: string - private headerOptions = { - spinner: false, - spinnerIndex: 0, - spinnerCharacters: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'], - } - - private boxen?: boxen.Options - - private contentText = '' - private contentMode: 'text' | 'stream' = 'text' - +export class ReactiveTUIContent { private outStream?: stream.Writable - private errStream?: stream.Writable - constructor({ out, err }: RedwoodTUIConfig = {}) { - this.manager = UpdateManager.getInstance(out, err) + private mode: 'text' | 'stream' + private header: string + private content: string + private spinner: { + enabled: boolean + characters: string[] } + private boxen: boxen.Options + private frameInterval: number + + private spinnerIndex = 0 + + constructor(options: { + mode?: 'text' | 'stream' + header?: string + content?: string + spinner?: { + enabled?: boolean + characters?: string[] + } + boxen?: boxen.Options + outStream?: stream.Readable + frameInterval?: number + }) { + this.mode = options.mode || 'text' + this.header = options.header || '' + this.content = options.content || '' + + const defaultSpinner = { + enabled: false, + characters: ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'].map((c) => + RedwoodStyling.redwood(c) + ), + } + this.spinner = { ...defaultSpinner, ...options.spinner } + this.boxen = { ...options.boxen } + this.frameInterval = options.frameInterval || 80 - /** - * Enable control of the terminal and allow writing content - */ - enable() { - if (!this.manager.isHooked) { - this.manager.hook() - this.looping = true - this.timerId = setInterval(() => { - this.loop() - }, 80) + if (options.outStream) { + this.setOutStream(options.outStream) } } - /** - * Disable control of the terminal and stop writing content - */ - disable() { - if (this.manager.isHooked) { - // Stop the draw loop - this.looping = false - clearInterval(this.timerId) - - // Draw one last time - this.loop(true) - - this.outStream?.destroy() - this.errStream?.destroy() - this.manager.unhook() + update(options: { + mode?: 'text' | 'stream' + header?: string + content?: string + spinner?: { + enabled?: boolean + characters?: string[] + } + boxen?: boxen.Options + outStream?: stream.Readable + frameInterval?: number + }) { + if (options.mode) { + this.mode = options.mode + } + if (options.header !== undefined) { + this.header = options.header + } + if (options.content !== undefined) { + this.content = options.content + } + if (options.spinner) { + // TODO: Validate characters array has at least two characters + this.spinner = { ...this.spinner, ...options.spinner } + } + if (options.boxen) { + this.boxen = { ...this.boxen, ...options.boxen } + } + if (options.outStream) { + this.setOutStream(options.outStream) + } + if (options.frameInterval) { + // TODO: Validate > 0 + this.frameInterval = options.frameInterval } } - /** - * The draw loop - */ - loop(force = false) { - if (this.looping || force) { - const builtContent = this.buildContent() - const content: string[] = [] - if (this.boxen) { - content.push(...boxen(builtContent || '', this.boxen).split('\n')) - } else { - if (this.header) { - if (this.headerOptions.spinner) { - content.push( - `${ - this.headerOptions.spinnerCharacters[ - this.headerOptions.spinnerIndex - ] - } ${this.header}` - ) - this.headerOptions.spinnerIndex += 1 - this.headerOptions.spinnerIndex = - this.headerOptions.spinnerIndex % - this.headerOptions.spinnerCharacters.length - } else { - content.push(this.header) - } - } - if (builtContent) { - content.push(builtContent) + setOutStream(out: stream.Readable) { + this.outStream = new stream.Writable({ + write: (chunk: Buffer, _encoding, next) => { + if (this.content === 'stream') { + this.content += chunk.toString('utf-8') } - } - this.manager.update(content) - } + next() + return true + }, + }) + out.pipe(this.outStream, { end: true }) } - /** - * TODO: This should be used to prevent overriding all of the history, like move on to a newline and update any new content - */ - moveOn() { - // If needed redraw to remove artifacts like spinners + renderToString(): string { + // Stream based content + if (this.mode === 'stream') { + return 'Not implemented yet' + } + + // Text based content + let renderedString = this.content + + // Add the header if it exists if (this.header) { - this.setHeader(this.header, { spinner: false }) - this.loop(true) + renderedString = `${this.header}\n${renderedString}` } - // Disable - this.disable() + // Add a spinner if enabled + if (this.spinner.enabled) { + renderedString = `${ + this.spinner.characters[this.spinnerIndex] + } ${renderedString}` - // Clear any previously defined content and settings - this.contentText = '' - this.contentMode = 'text' - this.clearHeader() - this.clearBoxen() + // Increment the spinner index and reset if necessary + this.spinnerIndex += 1 + if (this.spinnerIndex >= this.spinner.characters.length) { + this.spinnerIndex = 0 + } + } - // Re-enable - this.enable() + return renderedString + // buildContent(): string { + // const content: string[] = [] + // switch (this.contentMode) { + // case 'text': + // if (this.contentText) { + // content.push(this.contentText) + // } + // break + // case 'stream': + // if (this.contentText) { + // content.push(this.contentText) + // } + // break + // } + // return content.join('\n') + // } + return '' } - buildContent(): string { - const content: string[] = [] - switch (this.contentMode) { - case 'text': - if (this.contentText) { - content.push(this.contentText) - } - break - case 'stream': - if (this.contentText) { - content.push(this.contentText) - } - break - } - return content.join('\n') + getFrameInterval() { + return this.frameInterval } +} - // --- +/** + * TODO: Documentation for this + */ +export class RedwoodTUI { + private manager: UpdateManager - setHeader(header: string, options?: Partial) { - this.header = header - this.headerOptions = { ...this.headerOptions, ...options } - } + private outStream: NodeJS.WriteStream + private errStream: NodeJS.WriteStream - clearHeader() { - this.header = undefined - } + private timerId?: NodeJS.Timer + private isReactive = false - // --- + reactiveContent?: ReactiveTUIContent - setBoxen(options: boxen.Options) { - this.boxen = options - } + constructor({ out, err }: RedwoodTUIConfig = {}) { + this.outStream = out || process.stdout + this.errStream = err || process.stderr + this.manager = UpdateManager.getInstance(this.outStream, this.errStream) - clearBoxen() { - this.boxen = undefined + // Stop any remaining reactive content or there could be side effects like the cursor being hidden + process.on('exit', () => { + this.stopReactive() + }) } - // --- + startReactive(reactiveContent?: ReactiveTUIContent) { + // Stop any existing reactive content + if (this.isReactive) { + this.stopReactive() + } - setContent(text: string) { - this.contentText = text - } + // Set the reactive content if passed in + if (reactiveContent) { + this.reactiveContent = reactiveContent + } - setContentMode(mode: 'text' | 'stream') { - this.contentMode = mode - } + // Check if there is reactive content + if (!this.reactiveContent) { + throw new Error('TUI has no reactive content') + } - // --- + if (!this.manager.isHooked) { + // Take control of the terminal + this.manager.hook() - setOutStream(out: stream.Readable) { - this.outStream = new stream.Writable({ - write: (chunk: Buffer, _encoding, next) => { - if (this.contentMode === 'stream') { - this.contentText += chunk.toString('utf-8') - } - next() - return true - }, - }) - out.pipe(this.outStream, { end: true }) + // Start the draw loop + this.isReactive = true + this.timerId = setInterval(() => { + this.drawReactive() + }, this.reactiveContent.getFrameInterval()) + } } - setErrStream(err: stream.Readable) { - this.errStream = new stream.Writable({ - write: (_chunk, _encoding, _next) => { - // if(this.contentMode === "stream"){ - // this.contentText += chunk - // } - return true - }, - }) - err.pipe(this.errStream, { end: true }) + stopReactive(clear = false) { + if (this.manager.isHooked) { + // Stop the draw loop + this.isReactive = false + clearInterval(this.timerId) + + // Draw one last time to ensure the final state is shown + this.drawReactive(true) + + // Clear the last drawn content if requested + if (clear) { + this.manager.erase(this.manager.lastLength) + } + + // Give up control of the terminal + this.manager.unhook() + } } - // --- + /** + * + * @param force + */ + private drawReactive(force = false) { + if (this.isReactive || force) { + const wasHooked = this.manager.isHooked + if (force && !wasHooked) { + this.manager.hook() + } + const content = this.reactiveContent?.renderToString() + if (content) { + this.manager.update(content.split('\n')) + } + if (force && !wasHooked) { + this.manager.unhook() + } + } + } - drawLinesAndMoveOn(...lines: string[]) { - lines.forEach((line) => { - this.setContentMode('text') - this.setContent(line) - this.loop(true) - this.moveOn() - }) + getCurrentReactive(): ReactiveTUIContent | undefined { + return this.reactiveContent } - // --- + drawText(text: string) { + this.outStream.write(`${text}\n`) + } - promptA = enquirerPrompt + // /** + // * TODO: This should be used to prevent overriding all of the history, like move on to a newline and update any new content + // */ + // moveOn() { + // // If needed redraw to remove artifacts like spinners + // if (this.header) { + // this.setHeader(this.header, { spinner: false }) + // this.loop(true) + // } + + // // Disable + // this.disable() + + // // Clear any previously defined content and settings + // this.contentText = '' + // this.contentMode = 'text' + // this.clearHeader() + // this.clearBoxen() + + // // Re-enable + // this.enable() + // } - // TODO: Fix types of questions, enquirer does not export their types... - async prompt(questions: any): Promise { - this.moveOn() - this.disable() + /** + * A wrapper around enquirer.prompt that disables the reactive TUI and prompts + * + * @param questions A question or array of questions to prompt the user with + * + * @returns The prompt result + */ + async prompt( + questions: Parameters[0] + ): Promise { + const wasReactive = this.isReactive + if (wasReactive) { + this.stopReactive() + } const result = await enquirerPrompt(questions) - this.enable() + if (wasReactive) { + this.startReactive() + } return result } - // --- - + /** + * Display an error message in a box + * + * @param title Error box title + * @param message Error message + */ displayError(title: string, message: string) { - this.moveOn() - this.setBoxen({ - padding: 1, - borderColor: 'red', - title: `⚠ Error: ${title}`, - titleAlignment: 'left', - }) - this.setContentMode('text') - this.setContent(message) - this.moveOn() + this.drawText( + boxen(message, { + padding: 1, + borderColor: 'red', + title: `⚠ Error: ${title}`, + titleAlignment: 'left', + }) + ) } - - // --- } diff --git a/yarn.lock b/yarn.lock index 00e2eabe5fac..ee251fc24815 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7279,7 +7279,7 @@ __metadata: languageName: unknown linkType: soft -"@redwoodjs/tui@workspace:packages/tui": +"@redwoodjs/tui@workspace:^, @redwoodjs/tui@workspace:packages/tui": version: 0.0.0-use.local resolution: "@redwoodjs/tui@workspace:packages/tui" dependencies: @@ -14394,6 +14394,7 @@ __metadata: "@opentelemetry/resources": 1.11.0 "@opentelemetry/sdk-trace-node": 1.11.0 "@opentelemetry/semantic-conventions": 1.11.0 + "@redwoodjs/tui": "workspace:^" "@types/prompts": 2.4.4 chalk: 4.1.2 check-node-version: 4.2.1 From c92e13e449e09479ad1630b2b72ee0f637bd6018 Mon Sep 17 00:00:00 2001 From: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:16:36 +0100 Subject: [PATCH 04/10] Tidy the TUI index and add some comments --- packages/create-redwood-app/src/telemetry.js | 2 +- packages/tui/src/index.ts | 108 +++++++------------ yarn.lock | 4 +- 3 files changed, 42 insertions(+), 72 deletions(-) diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js index f15fed7c7b92..3bff4368b547 100644 --- a/packages/create-redwood-app/src/telemetry.js +++ b/packages/create-redwood-app/src/telemetry.js @@ -80,7 +80,7 @@ export async function startTelemetry() { }) traceExporter = new OTLPTraceExporter({ // TODO: Point this to somewhere permanent - url: 'https://master.telemetry-analytics.c66.me/v1/traces', + url: 'https://master-axolotl.telemetry-analytics-jgmw.c66.me/v1/traces', }) traceProcessor = new BatchSpanProcessor(traceExporter) traceProvider.addSpanProcessor(traceProcessor) diff --git a/packages/tui/src/index.ts b/packages/tui/src/index.ts index 3a397bd99467..8a1eab544d69 100644 --- a/packages/tui/src/index.ts +++ b/packages/tui/src/index.ts @@ -7,33 +7,6 @@ import chalk from 'chalk' import { prompt as enquirerPrompt } from 'enquirer' import { UpdateManager } from 'stdout-update' -/** - * Specifications: - * - A basic progress bar - * - Templates for common tasks: - * - ~~Errors~~ - * - Warnings - * - Info - * - Success - */ - -export interface RedwoodTUIHeaderOptions { - spinner: boolean - spinnerIndex: number - spinnerCharacters: string[] -} - -/** - * Configuration for the TUI - * - * Accepts an out and err stream which the TUI will write to. - */ -export interface RedwoodTUIConfig { - out?: NodeJS.WriteStream - err?: NodeJS.WriteStream -} - -// TODO: Rename this to RedwoodChalk? /** * A default set of styling for the TUI, designed for a cohesive look and feel around the Redwood CLI, CRWA and vairous plugins */ @@ -51,6 +24,9 @@ export const RedwoodStyling = { green: chalk.green, } +/** + * An object used to describe a "reactive" TUI element, that is an element that is updated a number of times per second + */ export class ReactiveTUIContent { private outStream?: stream.Writable @@ -64,6 +40,8 @@ export class ReactiveTUIContent { private boxen: boxen.Options private frameInterval: number + // TODO: Implement a progress bar + private spinnerIndex = 0 constructor(options: { @@ -175,23 +153,6 @@ export class ReactiveTUIContent { } return renderedString - // buildContent(): string { - // const content: string[] = [] - // switch (this.contentMode) { - // case 'text': - // if (this.contentText) { - // content.push(this.contentText) - // } - // break - // case 'stream': - // if (this.contentText) { - // content.push(this.contentText) - // } - // break - // } - // return content.join('\n') - // } - return '' } getFrameInterval() { @@ -199,6 +160,16 @@ export class ReactiveTUIContent { } } +/** + * Configuration for the TUI + * + * Accepts an out and err stream which the TUI will write to. + */ +export interface RedwoodTUIConfig { + out?: NodeJS.WriteStream + err?: NodeJS.WriteStream +} + /** * TODO: Documentation for this */ @@ -211,7 +182,7 @@ export class RedwoodTUI { private timerId?: NodeJS.Timer private isReactive = false - reactiveContent?: ReactiveTUIContent + private reactiveContent?: ReactiveTUIContent constructor({ out, err }: RedwoodTUIConfig = {}) { this.outStream = out || process.stdout @@ -224,6 +195,11 @@ export class RedwoodTUI { }) } + /** + * Enables rendering of a reactive component to the TUI + * + * @param reactiveContent A new ReactiveTUIContent object set as the current reactive content + */ startReactive(reactiveContent?: ReactiveTUIContent) { // Stop any existing reactive content if (this.isReactive) { @@ -252,6 +228,11 @@ export class RedwoodTUI { } } + /** + * Stops any new draws of the current reactive content to the TUI + * + * @param clear If true, the last drawn content will be cleared + */ stopReactive(clear = false) { if (this.manager.isHooked) { // Stop the draw loop @@ -272,8 +253,9 @@ export class RedwoodTUI { } /** + * Renders the current reactive content and draws it to the TUI * - * @param force + * @param force Force a draw even if the TUI is not reactive */ private drawReactive(force = false) { if (this.isReactive || force) { @@ -291,37 +273,25 @@ export class RedwoodTUI { } } + /** + * Gets the current reactive TUI content if there is one + * + * @returns The current reactive content or undefined if there isn't one + */ getCurrentReactive(): ReactiveTUIContent | undefined { return this.reactiveContent } + /** + * Writes a string to the TUI output stream + * + * @param text The string to write out + */ drawText(text: string) { this.outStream.write(`${text}\n`) } - // /** - // * TODO: This should be used to prevent overriding all of the history, like move on to a newline and update any new content - // */ - // moveOn() { - // // If needed redraw to remove artifacts like spinners - // if (this.header) { - // this.setHeader(this.header, { spinner: false }) - // this.loop(true) - // } - - // // Disable - // this.disable() - - // // Clear any previously defined content and settings - // this.contentText = '' - // this.contentMode = 'text' - // this.clearHeader() - // this.clearBoxen() - - // // Re-enable - // this.enable() - // } - + // TODO: Consider a custom prompting implementation for full control of look/feel/functionality etc... /** * A wrapper around enquirer.prompt that disables the reactive TUI and prompts * diff --git a/yarn.lock b/yarn.lock index ee251fc24815..82e349930aeb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7279,7 +7279,7 @@ __metadata: languageName: unknown linkType: soft -"@redwoodjs/tui@workspace:^, @redwoodjs/tui@workspace:packages/tui": +"@redwoodjs/tui@4.0.0, @redwoodjs/tui@workspace:packages/tui": version: 0.0.0-use.local resolution: "@redwoodjs/tui@workspace:packages/tui" dependencies: @@ -14394,7 +14394,7 @@ __metadata: "@opentelemetry/resources": 1.11.0 "@opentelemetry/sdk-trace-node": 1.11.0 "@opentelemetry/semantic-conventions": 1.11.0 - "@redwoodjs/tui": "workspace:^" + "@redwoodjs/tui": 4.0.0 "@types/prompts": 2.4.4 chalk: 4.1.2 check-node-version: 4.2.1 From 26edd7dacd59a8630c79ea874139dbbc8a628114 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 19 Apr 2023 15:35:17 -0400 Subject: [PATCH 05/10] chore: dedupe deps --- yarn.lock | 49 +++---------------------------------------------- 1 file changed, 3 insertions(+), 46 deletions(-) diff --git a/yarn.lock b/yarn.lock index 6342bb8783aa..a154009a3d7e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5536,14 +5536,14 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:1.4.0, @opentelemetry/api@npm:^1.3.0": +"@opentelemetry/api@npm:1.4.0": version: 1.4.0 resolution: "@opentelemetry/api@npm:1.4.0" checksum: 1673fd2815eeb5487f2b0dec8595285e634f05b5b06b9a06db7af82238eac197b900ed10bebd923ac5d1fbc4da708e2f574aba58a465f27857f358c2d16987c3 languageName: node linkType: hard -"@opentelemetry/api@npm:1.4.1": +"@opentelemetry/api@npm:1.4.1, @opentelemetry/api@npm:^1.3.0": version: 1.4.1 resolution: "@opentelemetry/api@npm:1.4.1" checksum: 5ee641d3d64c91e87ee328fc22251fc70c809a3c744e51e595ca77c0bd3cad933b77a79beb4dac66b811e5068941cef9da58c1ec217c0748a01f598e08a7ae66 @@ -5570,17 +5570,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.9.1": - version: 1.9.1 - resolution: "@opentelemetry/core@npm:1.9.1" - dependencies: - "@opentelemetry/semantic-conventions": 1.9.1 - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: c890878d6bda33b8998a2622104d39a098febbf792f7c123eca3d5c14e98a963af37cea63adaecabeeeeab72659c4e01f708934ffe040c1a167dd04a0105cb6f - languageName: node - linkType: hard - "@opentelemetry/exporter-trace-otlp-http@npm:0.37.0": version: 0.37.0 resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.37.0" @@ -5655,18 +5644,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/resources@npm:1.9.1": - version: 1.9.1 - resolution: "@opentelemetry/resources@npm:1.9.1" - dependencies: - "@opentelemetry/core": 1.9.1 - "@opentelemetry/semantic-conventions": 1.9.1 - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: d307e7377e9dd7e2955939e3386c6187324186bc6c5ee66f6315af43cee9a4ec733d5bf08bbb43527c3814df0ed63ec6c30364bd44be182b749c765901142a19 - languageName: node - linkType: hard - "@opentelemetry/sdk-metrics@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/sdk-metrics@npm:1.11.0" @@ -5680,7 +5657,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.11.0": +"@opentelemetry/sdk-trace-base@npm:1.11.0, @opentelemetry/sdk-trace-base@npm:^1.8.0": version: 1.11.0 resolution: "@opentelemetry/sdk-trace-base@npm:1.11.0" dependencies: @@ -5693,19 +5670,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:^1.8.0": - version: 1.9.1 - resolution: "@opentelemetry/sdk-trace-base@npm:1.9.1" - dependencies: - "@opentelemetry/core": 1.9.1 - "@opentelemetry/resources": 1.9.1 - "@opentelemetry/semantic-conventions": 1.9.1 - peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: 0f662c7f14da44b8888247abf99f5650c8d6d2ce12e8c345afbb7c218ebc7b1697a87af05f84f3645f717026eb963b45226c74b3e60d7459feb1534e62129034 - languageName: node - linkType: hard - "@opentelemetry/sdk-trace-node@npm:1.11.0": version: 1.11.0 resolution: "@opentelemetry/sdk-trace-node@npm:1.11.0" @@ -5729,13 +5693,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.9.1": - version: 1.9.1 - resolution: "@opentelemetry/semantic-conventions@npm:1.9.1" - checksum: 6acca1e2ad6eff361e779de4a5260f9c0542c6557b0105e5785deaa5aa1bbf74625f8267df4be33580f190efeb70da949ecbfe0cf74e89488abf1e0ba479daf1 - languageName: node - linkType: hard - "@parcel/watcher@npm:2.0.4": version: 2.0.4 resolution: "@parcel/watcher@npm:2.0.4" From b27b75e29dda885184156dcdc6d3e7d1ddedf2a9 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 19 Apr 2023 15:36:30 -0400 Subject: [PATCH 06/10] chore: fix core-js versions --- packages/tui/package.json | 2 +- yarn.lock | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/tui/package.json b/packages/tui/package.json index e84fdad6e190..4daa004c9e45 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -31,7 +31,7 @@ "@babel/runtime-corejs3": "7.21.0", "boxen": "5.1.2", "chalk": "4.1.2", - "core-js": "3.29.1", + "core-js": "3.30.0", "enquirer": "2.3.6", "stdout-update": "1.6.8" }, diff --git a/yarn.lock b/yarn.lock index a154009a3d7e..26889c5ed706 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7322,7 +7322,7 @@ __metadata: "@babel/runtime-corejs3": 7.21.0 boxen: 5.1.2 chalk: 4.1.2 - core-js: 3.29.1 + core-js: 3.30.0 enquirer: 2.3.6 jest: 29.5.0 stdout-update: 1.6.8 @@ -14236,13 +14236,6 @@ __metadata: languageName: node linkType: hard -"core-js@npm:3.29.1": - version: 3.29.1 - resolution: "core-js@npm:3.29.1" - checksum: e5fb4a18148e76a136b5c90d994715e15ed0e42cf0f073429174204bd119f1ba81dc582fff450e3792b73825cd1fe234ad76f85567ba7943039f5842be02591c - languageName: node - linkType: hard - "core-js@npm:3.30.0, core-js@npm:^3.0.4, core-js@npm:^3.26.0, core-js@npm:^3.27.2, core-js@npm:^3.6.5, core-js@npm:^3.8.2": version: 3.30.0 resolution: "core-js@npm:3.30.0" From 8494c7857f095fc86593abdfe1992a7a92806b62 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 19 Apr 2023 15:40:03 -0400 Subject: [PATCH 07/10] chore: align @opentelemetry/api versions --- packages/graphql-server/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graphql-server/package.json b/packages/graphql-server/package.json index 23362371c625..d03d386a7f47 100644 --- a/packages/graphql-server/package.json +++ b/packages/graphql-server/package.json @@ -32,7 +32,7 @@ "@graphql-tools/merge": "8.4.0", "@graphql-tools/schema": "9.0.17", "@graphql-tools/utils": "9.2.1", - "@opentelemetry/api": "1.4.0", + "@opentelemetry/api": "1.4.1", "@redwoodjs/api": "4.0.0", "core-js": "3.30.0", "graphql": "16.6.0", From 5a91b40b268b3044a2fb467bcff684e4076d608a Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 19 Apr 2023 15:47:47 -0400 Subject: [PATCH 08/10] chore: update lock file --- yarn.lock | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 26889c5ed706..fb608a63c133 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5536,13 +5536,6 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:1.4.0": - version: 1.4.0 - resolution: "@opentelemetry/api@npm:1.4.0" - checksum: 1673fd2815eeb5487f2b0dec8595285e634f05b5b06b9a06db7af82238eac197b900ed10bebd923ac5d1fbc4da708e2f574aba58a465f27857f358c2d16987c3 - languageName: node - linkType: hard - "@opentelemetry/api@npm:1.4.1, @opentelemetry/api@npm:^1.3.0": version: 1.4.1 resolution: "@opentelemetry/api@npm:1.4.1" @@ -7000,7 +6993,7 @@ __metadata: "@graphql-tools/merge": 8.4.0 "@graphql-tools/schema": 9.0.17 "@graphql-tools/utils": 9.2.1 - "@opentelemetry/api": 1.4.0 + "@opentelemetry/api": 1.4.1 "@redwoodjs/api": 4.0.0 "@types/jsonwebtoken": 9.0.1 "@types/lodash.merge": 4.6.7 From 1db02ed64affe8482ec6a88a0caa1df203272d52 Mon Sep 17 00:00:00 2001 From: Dominic Saadi Date: Wed, 19 Apr 2023 16:02:05 -0400 Subject: [PATCH 09/10] add back babel-node --- packages/create-redwood-app/package.json | 1 + yarn.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/create-redwood-app/package.json b/packages/create-redwood-app/package.json index 00f6a62a8f2c..9d8b3b0fef82 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -21,6 +21,7 @@ "test:watch": "yarn test --watch" }, "dependencies": { + "@babel/node": "7.20.7", "@babel/runtime-corejs3": "7.21.0", "@opentelemetry/api": "1.4.1", "@opentelemetry/exporter-trace-otlp-http": "0.37.0", diff --git a/yarn.lock b/yarn.lock index fb608a63c133..1451e14972ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14422,6 +14422,7 @@ __metadata: resolution: "create-redwood-app@workspace:packages/create-redwood-app" dependencies: "@babel/cli": 7.21.0 + "@babel/node": 7.20.7 "@babel/runtime-corejs3": 7.21.0 "@opentelemetry/api": 1.4.1 "@opentelemetry/exporter-trace-otlp-http": 0.37.0 From 0b4e4d77f0d2639877aa66c26ea8ef6494d60f68 Mon Sep 17 00:00:00 2001 From: Josh-Walker-GM <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Wed, 19 Apr 2023 21:18:46 +0100 Subject: [PATCH 10/10] Enable telemetry mock server and check only that packets are received --- .github/actions/telemetry_check/check.mjs | 47 ++------------------ packages/create-redwood-app/src/telemetry.js | 4 +- 2 files changed, 6 insertions(+), 45 deletions(-) diff --git a/.github/actions/telemetry_check/check.mjs b/.github/actions/telemetry_check/check.mjs index 52837b76f7f5..31327059c0ac 100644 --- a/.github/actions/telemetry_check/check.mjs +++ b/.github/actions/telemetry_check/check.mjs @@ -8,28 +8,6 @@ console.log( `Telemetry is being redirected to ${process.env.REDWOOD_REDIRECT_TELEMETRY}` ) -// All the fields we expect inside a telemetry packet -const expectedPacketFields = [ - 'type', - 'command', - 'duration', - 'uid', - 'ci', - 'redwoodCi', - 'NODE_ENV', - 'os', - 'osVersion', - // "shell", // Not expected on windows - 'nodeVersion', - 'yarnVersion', - 'npmVersion', - 'redwoodVersion', - 'system', - 'complexity', - 'sides', - 'webBundler', -] - // Setup fake telemetry server const server = http.createServer((req, res) => { let data = '' @@ -39,27 +17,8 @@ const server = http.createServer((req, res) => { req.on('end', () => { res.writeHead(200) res.end() - - const packet = JSON.parse(data) - - let hasAllFields = true - for (const field of expectedPacketFields) { - if (packet[field] === undefined) { - hasAllFields = false - console.error(`Telemetry packet is missing field "${field}"`) - } - } - - const isCI = packet.ci ?? false - - if (hasAllFields && isCI) { - console.log('Valid telemetry received') - process.exit(0) - } else { - console.error('Invalid telemetry received') - console.error(packet) - process.exit(1) - } + console.log('Telemetry packet received') + process.exit(0) }) }) @@ -77,7 +36,7 @@ try { switch (mode) { case 'crwa': exitCode = await exec( - `yarn node ./packages/create-redwood-app/dist/create-redwood-app.js ../project-for-telemetry --typescript false --git false --yarn-install true` + `yarn node ./packages/create-redwood-app/dist/create-redwood-app.js ../project-for-telemetry --typescript true --git false --yarn-install true` ) if (exitCode) { process.exit(1) diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js index 3bff4368b547..b7f59880bcac 100644 --- a/packages/create-redwood-app/src/telemetry.js +++ b/packages/create-redwood-app/src/telemetry.js @@ -80,7 +80,9 @@ export async function startTelemetry() { }) traceExporter = new OTLPTraceExporter({ // TODO: Point this to somewhere permanent - url: 'https://master-axolotl.telemetry-analytics-jgmw.c66.me/v1/traces', + url: + process.env.REDWOOD_REDIRECT_TELEMETRY || + 'https://master-axolotl.telemetry-analytics-jgmw.c66.me/v1/traces', }) traceProcessor = new BatchSpanProcessor(traceExporter) traceProvider.addSpanProcessor(traceProcessor)