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/package.json b/packages/create-redwood-app/package.json index b58bb9a43f3c..9d8b3b0fef82 100644 --- a/packages/create-redwood-app/package.json +++ b/packages/create-redwood-app/package.json @@ -21,18 +21,22 @@ "test:watch": "yarn test --watch" }, "dependencies": { - "@babel/core": "7.21.3", "@babel/node": "7.20.7", "@babel/runtime-corejs3": "7.21.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", + "@redwoodjs/tui": "4.0.0", "chalk": "4.1.2", "check-node-version": "4.2.1", + "ci-info": "3.8.0", "core-js": "3.30.0", - "enquirer": "2.3.6", + "envinfo": "7.8.1", "execa": "5.1.1", "fs-extra": "11.1.1", - "listr2": "5.0.8", + "systeminformation": "5.17.12", "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..de95d424f47b 100644 --- a/packages/create-redwood-app/src/create-redwood-app.js +++ b/packages/create-redwood-app/src/create-redwood-app.js @@ -6,71 +6,466 @@ // Usage: // `$ yarn create redwood-app ./path/to/new-project` -import { spawn } from 'child_process' +// import { spawn } from 'child_process' import path from 'path' +import { trace, SpanStatusCode } from '@opentelemetry/api' 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 { hideBin, Parser } from 'yargs/helpers' import yargs from 'yargs/yargs' +import { RedwoodTUI, ReactiveTUIContent, RedwoodStyling } from '@redwoodjs/tui' + import { name, version } from '../package' -/** - * 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) - * - */ -;(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, +import { + startTelemetry, + shutdownTelemetry, + recordErrorViaTelemetry, +} from './telemetry' + +// Telemetry +const { telemetry } = Parser(hideBin(process.argv)) + +const tui = new RedwoodTUI() + +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) { + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.warning( + '⚠' + )} Skipped compatibility check because yarn install was skipped via command line flag`, + }) + tui.stopReactive() + return } - // Initial welcome message - console.log( - `${style.redwood( - '------------------------------------------------------------------' - )}` + 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]) + }) + } ) - console.log(`🌲⚡️ ${style.header('Welcome to RedwoodJS!')} ⚡️🌲`) - console.log( - `${style.redwood( - '------------------------------------------------------------------' - )}` + + if (engineCheckPassed) { + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.green('✔')} Compatibility checks passed`, + }) + tui.stopReactive() + return + } + + if (!engineCheckPassed) { + tui.stopReactive(true) + 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') + ) + try { + 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, + }) + if (response['override-engine-error'] === 'Quit install') { + recordErrorViaTelemetry('User quit after engine check error') + await shutdownTelemetry() + process.exit(1) // TODO: Should we use a different exit code? + } + } catch (error) { + recordErrorViaTelemetry('User cancelled install at engine check error') + 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.stopReactive(true) + tui.displayError( + 'Project directory already contains files', + [ + `'${RedwoodStyling.info(newAppDir)}' already exists and is not empty`, + ``, + `You can use the '${RedwoodStyling.info( + 'overwrite' + )}' flag to create the project even if target directory isn't empty`, + ].join('\n') + ) + recordErrorViaTelemetry(`Project directory already contains files`) + await shutdownTelemetry() + 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, + }) + } + + tuiContent.update({ + spinner: { + enabled: false, + }, + content: `${RedwoodStyling.green('✔')} Project files created`, + }) + tui.stopReactive() +} + +async function installNodeModules(newAppDir) { + 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, + cwd: newAppDir, + }) + + try { + await yarnInstallSubprocess + } catch (error) { + tui.stopReactive(true) + tui.displayError( + "Couldn't install node modules", + [ + `We could not install node modules via ${RedwoodStyling.info( + "'yarn install'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + recordErrorViaTelemetry(error) + await shutdownTelemetry() + process.exit(1) + } + + tuiContent.update({ + header: '', + content: `${RedwoodStyling.green('✔')} Node modules successfully installed`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() +} + +async function convertToJavascript(newAppDir) { + 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, + cwd: newAppDir, + }) + + try { + 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 ${RedwoodStyling.info( + "'yarn rw ts-to-js'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + recordErrorViaTelemetry(error) + await shutdownTelemetry() + process.exit(1) + } + + tuiContent.update({ + content: `${RedwoodStyling.green( + '✔' + )} Converted TypeScript files to JavaScript`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() +} + +async function generateTypes(newAppDir) { + const tuiContent = new ReactiveTUIContent({ + mode: 'text', + content: 'Generating types', + spinner: { + enabled: true, + }, + }) + tui.startReactive(tuiContent) + + const generateSubprocess = execa('yarn rw-gen', { + shell: true, + cwd: newAppDir, + }) + + try { + await generateSubprocess + } catch (error) { + tui.stopReactive(true) + tui.displayError( + "Couldn't generate types", + [ + `We could not generate types using ${RedwoodStyling.info( + "'yarn rw-gen'" + )}. Please see below for the full error message.`, + '', + error, + ].join('\n') + ) + recordErrorViaTelemetry(error) + await shutdownTelemetry() + process.exit(1) + } + + tuiContent.update({ + content: `${RedwoodStyling.green('✔')} Generated types`, + spinner: { + enabled: false, + }, + }) + tui.stopReactive() +} + +async function initialiseGit(newAppDir) { + 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"', + { + shell: true, + cwd: newAppDir, + } + ) + + try { + await gitSubprocess + } catch (error) { + tui.stopReactive(true) + tui.displayError( + "Couldn't initialise a git repo", + [ + `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.`, + '', + error, + ].join('\n') + ) + recordErrorViaTelemetry(error) + await shutdownTelemetry() + process.exit(1) + } + + 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) + } +} + +/** + * This function creates a new RedwoodJS app. + * + * It performs the following actions: + * - TODO - Add a list of what this function does + */ +async function createRedwoodApp() { + // Introductory message + 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: telemetry, + // 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]') @@ -112,391 +507,179 @@ import { name, version } from '../package' .version(version) .parse() + // Record some of the arguments for telemetry + trace.getActiveSpan().setAttribute('yarn-install', yarnInstall) + trace.getActiveSpan().setAttribute('overwrite', overwrite) + trace.getActiveSpan().setAttribute('yarn1', yarn1) + // Get the directory for installation from the args const targetDir = String(args).replace(/,/g, '-') // 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') ) + recordErrorViaTelemetry('No target directory specified') + await shutdownTelemetry() 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 executeCompatibilityCheck(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 = await handleTypescriptPreference(typescriptFlag) + trace.getActiveSpan().setAttribute('typescript', useTypescript) - 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, - }) - }, - }, - ] - } + // Determine git preference + const useGit = await handleGitPreference(gitInitFlag) + trace.getActiveSpan().setAttribute('git', useGit) - 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}"`] - } + // Create project files + await createProjectFiles(newAppDir, overwrite, yarn1) + + // Install the node packages + if (yarnInstall) { + const yarnInstallStart = Date.now() + await installNodeModules(newAppDir) + trace + .getActiveSpan() + .setAttribute('yarn-install-time', Date.now() - yarnInstallStart) + } else { + tui.drawText( + `${RedwoodStyling.warning( + '⚠' + )} Skipped yarn install step based on command line flag` + ) + } - 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() + // Conditionally convert to javascript + if (!useTypescript) { + if (yarnInstall) { + await convertToJavascript(newAppDir) } else { - fs.appendFileSync( - path.join(newAppDir, '.env'), - 'REDWOOD_DISABLE_TELEMETRY=1\n' + 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') ) } } - 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' - ) - - 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`) + // Generate types + if (yarnInstall) { + await generateTypes(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) - } + // Initialise git repo + if (useGit) { + await initialiseGit(newAppDir) } - // Main install Listr - new Listr( + // Post install message + tui.drawText( [ - { - 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, - } + '', + 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') ) - .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) - }) +;(async () => { + // Conditionally start telemetry + if (telemetry !== 'false' && !process.env.REDWOOD_DISABLE_TELEMETRY) { + try { + await startTelemetry() + } catch (error) { + console.error('Telemetry startup error') + console.error(error) + } + } + + // Execute create redwood app within a span + const tracer = trace.getTracer('redwoodjs') + await tracer.startActiveSpan('create-redwood-app', async (span) => { + await createRedwoodApp() + + // Span housekeeping + span.setStatus({ code: SpanStatusCode.OK }) + span.end() + }) + + // Shutdown telemetry, ensures data is sent before the process exits + try { + await shutdownTelemetry() + } catch (error) { + console.error('Telemetry shutdown error') + console.error(error) + } })() diff --git a/packages/create-redwood-app/src/telemetry.js b/packages/create-redwood-app/src/telemetry.js index e62de33587e8..b7f59880bcac 100644 --- a/packages/create-redwood-app/src/telemetry.js +++ b/packages/create-redwood-app/src/telemetry.js @@ -1,6 +1,114 @@ -const { sendTelemetry } = require('@redwoodjs/telemetry/dist/sendTelemetry') +import { diag, DiagConsoleLogger, DiagLogLevel } from '@opentelemetry/api' +import opentelemetry, { SpanStatusCode } from '@opentelemetry/api' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' +import { Resource } from '@opentelemetry/resources' +import { + NodeTracerProvider, + BatchSpanProcessor, +} from '@opentelemetry/sdk-trace-node' +import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions' +import ci from 'ci-info' +import envinfo from 'envinfo' +import system from 'systeminformation' -// if this script is run directly by node then telemetry will be sent in immediately -;(async function () { - await sendTelemetry() -})() +import { name as packageName, version as packageVersion } from '../package' + +/** + * @type NodeTracerProvider + */ +let traceProvider + +/** + * @type BatchSpanProcessor + */ +let traceProcessor + +/** + * @type OTLPTraceExporter + */ +let traceExporter + +export async function startTelemetry() { + diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ERROR) + + // Resources + const info = JSON.parse( + await envinfo.run( + { + System: ['OS', 'Shell'], + Binaries: ['Node', 'Yarn', 'npm'], + npmPackages: '@redwoodjs/*', + IDEs: ['VSCode'], + }, + { json: true } + ) + ) + + // get shell name instead of path + const shell = info.System?.Shell // Windows doesn't always provide shell info, I guess + if (shell?.path?.match('/')) { + info.System.Shell.name = info.System.Shell.path.split('/').pop() + } else if (shell?.path.match('\\')) { + info.System.Shell.name = info.System.Shell.path.split('\\').pop() + } + const cpu = await system.cpu() + const mem = await system.mem() + + const resource = Resource.default().merge( + new Resource({ + [SemanticResourceAttributes.SERVICE_NAME]: packageName, + [SemanticResourceAttributes.SERVICE_VERSION]: packageVersion, + [SemanticResourceAttributes.OS_TYPE]: info.System?.OS?.split(' ')[0], + [SemanticResourceAttributes.OS_VERSION]: info.System?.OS?.split(' ')[1], + 'shell.name': info.System?.Shell?.name, + 'node.version': info.Binaries?.Node?.version, + 'yarn.version': info.Binaries?.Yarn?.version, + 'npm.version': info.Binaries?.npm?.version, + 'vscode.version': info.IDEs?.VSCode?.version, + 'cpu.count': cpu.physicalCores, + 'memory.gb': Math.round(mem.total / 1073741824), + 'env.node_env': process.env.NODE_ENV || null, + 'ci.redwood': !!process.env.REDWOOD_CI, + 'ci.isci': ci.isCI, + fingerprint: undefined, // We don't provide a fingerprint here because it needs a fully setup project + }) + ) + + // Tracing + traceProvider = new NodeTracerProvider({ + resource: resource, + }) + traceExporter = new OTLPTraceExporter({ + // TODO: Point this to somewhere permanent + url: + process.env.REDWOOD_REDIRECT_TELEMETRY || + 'https://master-axolotl.telemetry-analytics-jgmw.c66.me/v1/traces', + }) + traceProcessor = new BatchSpanProcessor(traceExporter) + traceProvider.addSpanProcessor(traceProcessor) + traceProvider.register() + + process.on('SIGTERM', async () => { + await shutdownTelemetry() + }) +} + +export async function shutdownTelemetry() { + try { + opentelemetry.trace.getActiveSpan()?.end() + await traceProvider?.shutdown() + await traceProcessor?.shutdown() + await traceExporter?.shutdown() + } catch (error) { + console.error('Telemetry error') + console.error(error) + } +} + +export function recordErrorViaTelemetry(error) { + opentelemetry.trace.getActiveSpan().setStatus({ + code: SpanStatusCode.ERROR, + message: error.toString().split('\n')[0], + }) + opentelemetry.trace.getActiveSpan().recordException(error) +} 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", 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..4daa004c9e45 --- /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.30.0", + "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.3" + } +} 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..8a1eab544d69 --- /dev/null +++ b/packages/tui/src/index.ts @@ -0,0 +1,332 @@ +// + +import stream from 'stream' + +import boxen from 'boxen' +import chalk from 'chalk' +import { prompt as enquirerPrompt } from 'enquirer' +import { UpdateManager } from 'stdout-update' + +/** + * 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, + info: chalk.grey, + + header: chalk.bold.underline.hex('#e8e8e8'), + cmd: chalk.hex('#808080'), + redwood: chalk.hex('#ff845e'), + love: chalk.redBright, + + 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 + + private mode: 'text' | 'stream' + private header: string + private content: string + private spinner: { + enabled: boolean + characters: string[] + } + private boxen: boxen.Options + private frameInterval: number + + // TODO: Implement a progress bar + + 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 + + if (options.outStream) { + this.setOutStream(options.outStream) + } + } + + 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 + } + } + + setOutStream(out: stream.Readable) { + this.outStream = new stream.Writable({ + write: (chunk: Buffer, _encoding, next) => { + if (this.content === 'stream') { + this.content += chunk.toString('utf-8') + } + next() + return true + }, + }) + out.pipe(this.outStream, { end: true }) + } + + 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) { + renderedString = `${this.header}\n${renderedString}` + } + + // Add a spinner if enabled + if (this.spinner.enabled) { + renderedString = `${ + this.spinner.characters[this.spinnerIndex] + } ${renderedString}` + + // Increment the spinner index and reset if necessary + this.spinnerIndex += 1 + if (this.spinnerIndex >= this.spinner.characters.length) { + this.spinnerIndex = 0 + } + } + + return renderedString + } + + getFrameInterval() { + return this.frameInterval + } +} + +/** + * 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 + */ +export class RedwoodTUI { + private manager: UpdateManager + + private outStream: NodeJS.WriteStream + private errStream: NodeJS.WriteStream + + private timerId?: NodeJS.Timer + private isReactive = false + + private reactiveContent?: ReactiveTUIContent + + constructor({ out, err }: RedwoodTUIConfig = {}) { + this.outStream = out || process.stdout + this.errStream = err || process.stderr + this.manager = UpdateManager.getInstance(this.outStream, this.errStream) + + // Stop any remaining reactive content or there could be side effects like the cursor being hidden + process.on('exit', () => { + this.stopReactive() + }) + } + + /** + * 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) { + this.stopReactive() + } + + // Set the reactive content if passed in + if (reactiveContent) { + this.reactiveContent = reactiveContent + } + + // 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() + + // Start the draw loop + this.isReactive = true + this.timerId = setInterval(() => { + this.drawReactive() + }, this.reactiveContent.getFrameInterval()) + } + } + + /** + * 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 + 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() + } + } + + /** + * Renders the current reactive content and draws it to the TUI + * + * @param force Force a draw even if the TUI is not reactive + */ + 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() + } + } + } + + /** + * 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: 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 + * + * @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) + 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.drawText( + boxen(message, { + padding: 1, + borderColor: 'red', + title: `⚠ Error: ${title}`, + titleAlignment: 'left', + }) + ) + } +} 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 6043587ee407..1451e14972ac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5536,53 +5536,153 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:1.4.0, @opentelemetry/api@npm:^1.3.0": - version: 1.4.0 - resolution: "@opentelemetry/api@npm:1.4.0" - checksum: 1673fd2815eeb5487f2b0dec8595285e634f05b5b06b9a06db7af82238eac197b900ed10bebd923ac5d1fbc4da708e2f574aba58a465f27857f358c2d16987c3 +"@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 languageName: node linkType: hard -"@opentelemetry/core@npm:1.9.1": - version: 1.9.1 - resolution: "@opentelemetry/core@npm:1.9.1" +"@opentelemetry/context-async-hooks@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/context-async-hooks@npm:1.11.0" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 2e7182a54bea86083cd78670b21871440f8eba2b9e25408e6ac3c6748145441e7d3f8bfbd4db7725a52cccc8c66f871b9579cb2a9ece9b2441b5f323db245dae + languageName: node + linkType: hard + +"@opentelemetry/core@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/core@npm:1.11.0" dependencies: - "@opentelemetry/semantic-conventions": 1.9.1 + "@opentelemetry/semantic-conventions": 1.11.0 peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: c890878d6bda33b8998a2622104d39a098febbf792f7c123eca3d5c14e98a963af37cea63adaecabeeeeab72659c4e01f708934ffe040c1a167dd04a0105cb6f + checksum: 3c5c7b86449bfd4db6fd53ee0181c208731646e96df3f9bcff960d0d1c7f6fb3487665a632f98e7e8b71b897701123fd80b3464d5fe44b74d8736d0bbff55f6f languageName: node linkType: hard -"@opentelemetry/resources@npm:1.9.1": - version: 1.9.1 - resolution: "@opentelemetry/resources@npm:1.9.1" +"@opentelemetry/exporter-trace-otlp-http@npm:0.37.0": + version: 0.37.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.37.0" + dependencies: + "@opentelemetry/core": 1.11.0 + "@opentelemetry/otlp-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: b5cf829ab6f5db138066a4b70aad68b13999fb44c52622c4fe9168e5e363838507bc1b45c8e2797b7e6f0668e952462706f69ebdea6a05b3afc0470b7c883369 + 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" + dependencies: + "@opentelemetry/core": 1.11.0 + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 9af35bee0170d9db0c548a261846f7a96dd4df61708a7263569700cc74dc4e25cb8e39ed17e15e30f2e6844b5e2fd2b05d4ca5c37bfbb003b7d0bd7ea146add3 + languageName: node + linkType: hard + +"@opentelemetry/otlp-transformer@npm:0.37.0": + version: 0.37.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.37.0" + dependencies: + "@opentelemetry/core": 1.11.0 + "@opentelemetry/resources": 1.11.0 + "@opentelemetry/sdk-metrics": 1.11.0 + "@opentelemetry/sdk-trace-base": 1.11.0 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: b3a0654f39fc1fb98694b9f8eb37d8be321f0fa6d2b33e7cc4dc315f343699488a9a12ad671086df76892b310f691b75c1de037d2cf2289c54de3147519e3df2 + languageName: node + linkType: hard + +"@opentelemetry/propagator-b3@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/propagator-b3@npm:1.11.0" dependencies: - "@opentelemetry/core": 1.9.1 - "@opentelemetry/semantic-conventions": 1.9.1 + "@opentelemetry/core": 1.11.0 peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: d307e7377e9dd7e2955939e3386c6187324186bc6c5ee66f6315af43cee9a4ec733d5bf08bbb43527c3814df0ed63ec6c30364bd44be182b749c765901142a19 + checksum: acc0e5eb723bfab5fe48004077a20e3dc1048450558875a2fa2b3c77fda244ae7989eb58f07d5956e1a341d94e624dfcadc3873d48f51678518fb23cbd17e63f 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" +"@opentelemetry/propagator-jaeger@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/propagator-jaeger@npm:1.11.0" dependencies: - "@opentelemetry/core": 1.9.1 - "@opentelemetry/resources": 1.9.1 - "@opentelemetry/semantic-conventions": 1.9.1 + "@opentelemetry/core": 1.11.0 peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.5.0" - checksum: 0f662c7f14da44b8888247abf99f5650c8d6d2ce12e8c345afbb7c218ebc7b1697a87af05f84f3645f717026eb963b45226c74b3e60d7459feb1534e62129034 + checksum: e5beee47fb21a5d4da1d871930355140016badc97b63a0b7e605447153e16d3cd63f06a60b71de891b17000ae5e0a4a8270b66a40d8f627ec8859a4164a53cf9 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 +"@opentelemetry/resources@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/resources@npm:1.11.0" + dependencies: + "@opentelemetry/core": 1.11.0 + "@opentelemetry/semantic-conventions": 1.11.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: f2be8091dc86ab5d11abad4edc676715e6ee235ce2ec9fd28114ba446148bc34e2bb5266c4adc5e39eae6ca29ab08480edfa26f7b55a669ed4366fb27a28d107 + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/sdk-metrics@npm:1.11.0" + dependencies: + "@opentelemetry/core": 1.11.0 + "@opentelemetry/resources": 1.11.0 + lodash.merge: 4.6.2 + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.5.0" + checksum: a43fe30a1dc167c1916d68ddb4780ca192d2d0ff47cc83a1d675a7bd4c4d83b3373c55feda02d580146366232a96756d87eaf4c58c3e8a7f12a05552d3044152 + languageName: node + linkType: hard + +"@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: + "@opentelemetry/core": 1.11.0 + "@opentelemetry/resources": 1.11.0 + "@opentelemetry/semantic-conventions": 1.11.0 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: 6673485f1b39a9a3e396d5c81eb513d8cdfd1354ed04ae70c170da63c186171d93c645969b3090c6c498b5502f1c3c49427176d5e58e521eb348dc14b40f9ebc + 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" + dependencies: + "@opentelemetry/context-async-hooks": 1.11.0 + "@opentelemetry/core": 1.11.0 + "@opentelemetry/propagator-b3": 1.11.0 + "@opentelemetry/propagator-jaeger": 1.11.0 + "@opentelemetry/sdk-trace-base": 1.11.0 + semver: ^7.3.5 + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.5.0" + checksum: f3c6fd818348d8631a5cca321cf9cfbea08de4d70bcddc797599d0932bfc9b4ee924a50444fdfc7488e3989b9d23d878ef9f2baf5f63e65593213c86e239e6b4 + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:1.11.0": + version: 1.11.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.11.0" + checksum: 315c8074e01539fd5990f37dd2fdb14ca6189706634d60e0be06e9d84d66c7c68adb9522c8d0d61b638c4dcd4065f8b011017b68d09bd8da23ee7afba6097f74 languageName: node linkType: hard @@ -6893,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 @@ -7206,6 +7306,23 @@ __metadata: languageName: unknown linkType: soft +"@redwoodjs/tui@4.0.0, @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.30.0 + enquirer: 2.3.6 + jest: 29.5.0 + stdout-update: 1.6.8 + typescript: 5.0.3 + languageName: unknown + linkType: soft + "@redwoodjs/vite@workspace:packages/vite": version: 0.0.0-use.local resolution: "@redwoodjs/vite@workspace:packages/vite" @@ -10971,7 +11088,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: @@ -11037,7 +11154,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 @@ -14305,20 +14422,24 @@ __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 - "@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 + "@redwoodjs/tui": 4.0.0 "@types/prompts": 2.4.4 chalk: 4.1.2 check-node-version: 4.2.1 + ci-info: 3.8.0 core-js: 3.30.0 - enquirer: 2.3.6 + envinfo: 7.8.1 execa: 5.1.1 fs-extra: 11.1.1 jest: 29.5.0 - listr2: 5.0.8 + systeminformation: 5.17.12 terminal-link: 2.1.1 typescript: 5.0.3 yargs: 17.7.1 @@ -29016,6 +29137,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"