chore(): start moving some packages to the core directory (#7215)

This commit is contained in:
Adrien de Peretti
2024-05-03 13:37:41 +02:00
committed by GitHub
parent fdee748eed
commit bbccd6481d
1436 changed files with 275 additions and 756 deletions
@@ -0,0 +1,25 @@
import fs from "fs"
import glob from "glob"
import path from "path"
export function clearProject(directory: string) {
const adminFiles = glob.sync(path.join(directory, `src`, `admin/**/*`))
const onboardingFiles = glob.sync(
path.join(directory, `src`, `**/onboarding/`)
)
const typeFiles = glob.sync(path.join(directory, `src`, `types`))
const srcFiles = glob.sync(
path.join(directory, `src`, `**/*.{ts,tsx,js,jsx}`)
)
const files = [...adminFiles, ...onboardingFiles, ...typeFiles, ...srcFiles]
files.forEach((file) =>
fs.rmSync(file, {
recursive: true,
force: true,
})
)
// add empty typescript file to avoid build errors
fs.openSync(path.join(directory, "src", "index.ts"), "w")
}
@@ -0,0 +1,82 @@
import execute from "./execute.js"
import { Ora } from "ora"
import { isAbortError } from "./create-abort-controller.js"
import logMessage from "./log-message.js"
import fs from "fs"
import path from "path"
type CloneRepoOptions = {
directoryName?: string
repoUrl?: string
abortController?: AbortController
verbose?: boolean
v2?: boolean
}
const DEFAULT_REPO = "https://github.com/medusajs/medusa-starter-default"
const V2_BRANCH = "feat/v2"
export default async function cloneRepo({
directoryName = "",
repoUrl,
abortController,
verbose = false,
v2 = false,
}: CloneRepoOptions) {
await execute(
[
`git clone ${repoUrl || DEFAULT_REPO}${
v2 ? ` -b ${V2_BRANCH}` : ""
} ${directoryName}`,
{
signal: abortController?.signal,
},
],
{ verbose }
)
}
export async function runCloneRepo({
projectName,
repoUrl,
abortController,
spinner,
verbose = false,
v2 = false,
}: {
projectName: string
repoUrl: string
abortController: AbortController
spinner: Ora
verbose?: boolean
v2?: boolean
}) {
try {
await cloneRepo({
directoryName: projectName,
repoUrl,
abortController,
verbose,
v2,
})
deleteGitDirectory(projectName)
} catch (e) {
if (isAbortError(e)) {
process.exit()
}
spinner.stop()
logMessage({
message: `An error occurred while setting up your project: ${e}`,
type: "error",
})
}
}
function deleteGitDirectory(projectDirectory: string) {
fs.rmSync(path.join(projectDirectory, ".git"), {
recursive: true,
force: true,
})
}
@@ -0,0 +1,16 @@
import ProcessManager from "./process-manager.js"
export default (processManager: ProcessManager) => {
const abortController = new AbortController()
processManager.onTerminated(() => abortController.abort())
return abortController
}
export const isAbortError = (e: any) =>
e !== null && "code" in e && e.code === "ABORT_ERR"
export const getAbortError = () => {
return {
code: "ABORT_ERR",
}
}
@@ -0,0 +1,184 @@
import { EOL } from "os"
import pg from "pg"
import postgresClient from "./postgres-client.js"
import inquirer from "inquirer"
import logMessage from "./log-message.js"
import formatConnectionString from "./format-connection-string.js"
import { Ora } from "ora"
import { getCurrentOs } from "./get-current-os.js"
type CreateDbOptions = {
client: pg.Client
db: string
}
export default async function createDb({ client, db }: CreateDbOptions) {
await client.query(`CREATE DATABASE "${db}"`)
}
export async function runCreateDb({
client,
dbName,
spinner,
}: {
client: pg.Client
dbName: string
spinner: Ora
}): Promise<pg.Client> {
let newClient = client
try {
// create postgres database
await createDb({
client,
db: dbName,
})
// create a new connection with database selected
await client.end()
newClient = await postgresClient({
user: client.user,
password: client.password,
database: dbName,
})
} catch (e) {
spinner.stop()
logMessage({
message: `An error occurred while trying to create your database: ${e}`,
type: "error",
})
}
return newClient
}
async function getForDbName({
dbName,
verbose = false,
}: {
dbName: string
verbose?: boolean
}): Promise<{
client: pg.Client
dbConnectionString: string
}> {
let client!: pg.Client
let postgresUsername = "postgres"
let postgresPassword = ""
try {
client = await postgresClient({
user: postgresUsername,
password: postgresPassword,
})
} catch (e) {
if (verbose) {
logMessage({
message: `The following error occured when connecting to the database: ${e}`,
type: "verbose",
})
}
// ask for the user's postgres credentials
const answers = await inquirer.prompt([
{
type: "input",
name: "postgresUsername",
message: "Enter your Postgres username",
default: "postgres",
validate: (input) => {
return typeof input === "string" && input.length > 0
},
},
{
type: "password",
name: "postgresPassword",
message: "Enter your Postgres password",
},
])
postgresUsername = answers.postgresUsername
postgresPassword = answers.postgresPassword
try {
client = await postgresClient({
user: postgresUsername,
password: postgresPassword,
})
} catch (e) {
logMessage({
message: `Couldn't connect to PostgreSQL because of the following error: ${e}.${EOL}${EOL}Make sure you have PostgreSQL installed and the credentials you provided are correct.${EOL}${EOL}You can learn how to install PostgreSQL here: https://docs.medusajs.com/development/backend/prepare-environment?os=${getCurrentOs()}#postgresql${EOL}${EOL}If you keep running into this issue despite having PostgreSQL installed, please check out our troubleshooting guidelines: https://docs.medusajs.com/troubleshooting/database-error`,
type: "error",
})
}
}
// format connection string
const dbConnectionString = formatConnectionString({
user: postgresUsername,
password: postgresPassword,
host: client!.host,
db: dbName,
})
return {
client,
dbConnectionString,
}
}
async function getForDbUrl({
dbUrl,
verbose = false,
}: {
dbUrl: string
verbose?: boolean
}): Promise<{
client: pg.Client
dbConnectionString: string
}> {
let client!: pg.Client
try {
client = await postgresClient({
connectionString: dbUrl,
})
} catch (e) {
if (verbose) {
logMessage({
message: `The following error occured when connecting to the database: ${e}`,
type: "verbose",
})
}
logMessage({
message: `Couldn't connect to PostgreSQL using the database URL you passed. Make sure it's correct and try again.`,
type: "error",
})
}
return {
client,
dbConnectionString: dbUrl,
}
}
export async function getDbClientAndCredentials({
dbName = "",
dbUrl = "",
verbose = false,
}): Promise<{
client: pg.Client
dbConnectionString: string
verbose?: boolean
}> {
if (dbName) {
return await getForDbName({
dbName,
verbose,
})
} else {
return await getForDbUrl({
dbUrl,
verbose,
})
}
}
@@ -0,0 +1,72 @@
import { exec, spawnSync, SpawnSyncOptions } from "child_process"
import util from "util"
import { getAbortError } from "./create-abort-controller.js"
const promiseExec = util.promisify(exec)
type ExecuteOptions = {
stdout?: string
stderr?: string
}
type VerboseOptions = {
verbose?: boolean
// Since spawn doesn't allow us to both retrieve the
// output and output it live without using events,
// enabling this option, which is only useful if `verbose` is `true`,
// defers the output of the process until after the process is executed
// instead of outputting the log in realtime, which is the default.
// it prioritizes retrieving the output over outputting it in real-time.
needOutput?: boolean
}
type PromiseExecParams = Parameters<typeof promiseExec>
type SpawnParams = [string, SpawnSyncOptions]
const execute = async (
command: SpawnParams | PromiseExecParams,
{ verbose = false, needOutput = false }: VerboseOptions
): Promise<ExecuteOptions> => {
if (verbose) {
const [commandStr, options] = command as SpawnParams
const childProcess = spawnSync(commandStr, {
...options,
shell: true,
stdio: needOutput
? "pipe"
: [process.stdin, process.stdout, process.stderr],
})
if (childProcess.error) {
throw childProcess.error
}
if (
childProcess.signal &&
["SIGINT", "SIGTERM"].includes(childProcess.signal)
) {
console.log("abortingggg")
throw getAbortError()
}
if (needOutput) {
console.log(
childProcess.stdout?.toString() || childProcess.stderr?.toString()
)
}
return {
stdout: childProcess.stdout?.toString() || "",
stderr: childProcess.stderr?.toString() || "",
}
} else {
const childProcess = await promiseExec(...(command as PromiseExecParams))
return {
stdout: childProcess.stdout as string,
stderr: childProcess.stderr as string,
}
}
}
export default execute
@@ -0,0 +1,136 @@
import boxen from "boxen"
import chalk from "chalk"
import { emojify } from "node-emoji"
import { Ora } from "ora"
import ProcessManager from "./process-manager.js"
export type FactBoxOptions = {
interval: NodeJS.Timeout | null
spinner: Ora
processManager: ProcessManager
message?: string
title?: string
verbose?: boolean
}
const facts = [
"Plugins allow you to integrate third-party services for payment, fulfillment, notifications, and more.",
"You can specify a product's availability in one or more sales channels.",
"Payment and shipping options and providers can be configured per region.",
"Tax-inclusive pricing allows you to set prices for products, shipping options, and more without having to worry about calculating taxes.",
"Medusa provides multi-currency and region support, with full control over prices for each currency and region.",
"You can organize customers by customer groups and set special prices for them.",
"You can specify the inventory of products per location and sales channel.",
"Publishable-API Keys allow you to send requests to the backend within a scoped resource.",
"You can create custom endpoints by creating a TypeScript file under the src/api directory.",
"You can listen to events to perform asynchronous actions using Subscribers.",
"An entity represents a table in the database. You can create a table by creating a custom entity and migration.",
"Medusa's store endpoint paths are prefixed by /store. The admin endpoints are prefixed by /admin.",
"Medusa provides a JavaScript client and a React library that you can use to build a storefront or a custom admin.",
"Services are classes with methods related to an entity or functionality. You can create a custom service in a TypeScript file under src/services.",
"Modules allow you to replace an entire functionality with your custom logic.",
"The event bus module is responsible for triggering events and relaying them to subscribers.",
"The cache module is responsible for caching data that requires heavy computation.",
]
export const getFact = () => {
const randIndex = Math.floor(Math.random() * facts.length)
return facts[randIndex]
}
export const showFact = ({
spinner,
title,
verbose,
}: Pick<FactBoxOptions, "spinner" | "verbose"> & {
title: string
}) => {
const fact = getFact()
if (verbose) {
spinner.stopAndPersist({
symbol: chalk.cyan("⠋"),
text: title,
})
} else {
spinner.text = `${title}\n${boxen(`${fact}`, {
title: chalk.cyan(`${emojify(":bulb:")} Medusa Tips`),
titleAlignment: "center",
textAlignment: "center",
padding: 1,
margin: 1,
})}`
}
}
export const createFactBox = ({
spinner,
title,
processManager,
verbose,
}: Pick<FactBoxOptions, "spinner" | "processManager" | "verbose"> & {
title: string
}): NodeJS.Timeout => {
showFact({ spinner, title, verbose })
const interval = setInterval(() => {
showFact({ spinner, title, verbose })
}, 10000)
processManager.addInterval(interval)
return interval
}
export const resetFactBox = ({
interval,
spinner,
successMessage,
processManager,
newTitle,
verbose,
}: Pick<
FactBoxOptions,
"interval" | "spinner" | "processManager" | "verbose"
> & {
successMessage: string
newTitle?: string
}): NodeJS.Timeout | null => {
if (interval) {
clearInterval(interval)
}
spinner.succeed(chalk.green(successMessage)).start()
let newInterval = null
if (newTitle) {
newInterval = createFactBox({
spinner,
title: newTitle,
processManager,
verbose,
})
}
return newInterval
}
export function displayFactBox({
interval,
spinner,
processManager,
title = "",
message = "",
verbose = false,
}: FactBoxOptions): NodeJS.Timeout | null {
if (!message) {
return createFactBox({ spinner, title, processManager, verbose })
}
return resetFactBox({
interval,
spinner,
successMessage: message,
processManager,
newTitle: title,
verbose,
})
}
@@ -0,0 +1,29 @@
type ConnectionStringOptions = {
user?: string
password?: string
host?: string
db: string
}
export function encodeDbValue(value: string): string {
return encodeURIComponent(value)
}
export default ({ user, password, host, db }: ConnectionStringOptions) => {
let connection = `postgres://`
if (user) {
connection += encodeDbValue(user)
}
if (password) {
connection += `:${encodeDbValue(password)}`
}
if (user || password) {
connection += "@"
}
connection += `${host}/${db}`
return connection
}
@@ -0,0 +1,17 @@
import Configstore from "configstore"
let config: Configstore
export const getConfigStore = (): Configstore => {
if (!config) {
config = new Configstore(
`medusa`,
{},
{
globalConfigPath: true,
}
)
}
return config
}
@@ -0,0 +1,10 @@
export const getCurrentOs = (): string => {
switch (process.platform) {
case "darwin":
return "macos"
case "linux":
return "linux"
default:
return "windows"
}
}
@@ -0,0 +1,27 @@
import chalk from "chalk"
import { program } from "commander"
import { logger } from "./logger.js"
type LogOptions = {
message: string
type?: "error" | "success" | "info" | "warning" | "verbose"
}
export default ({ message, type = "info" }: LogOptions) => {
switch (type) {
case "info":
logger.info(chalk.white(message))
break
case "success":
logger.info(chalk.green(message))
break
case "warning":
logger.warning(chalk.yellow(message))
break
case "verbose":
logger.info(`${chalk.bgYellowBright("VERBOSE LOG:")} ${message}`)
break
case "error":
program.error(chalk.bold.red(message))
}
}
@@ -0,0 +1,10 @@
import winston from "winston"
const consoleTransport = new winston.transports.Console({
format: winston.format.printf((log) => log.message),
})
const options = {
transports: [consoleTransport],
}
export const logger = winston.createLogger(options)
@@ -0,0 +1,116 @@
import inquirer from "inquirer"
import { exec } from "child_process"
import execute from "./execute.js"
import { FactBoxOptions, displayFactBox } from "./facts.js"
import fs from "fs"
import path from "path"
import { customAlphabet } from "nanoid"
import { isAbortError } from "./create-abort-controller.js"
import logMessage from "./log-message.js"
const NEXTJS_REPO = "https://github.com/medusajs/nextjs-starter-medusa"
export async function askForNextjsStarter(): Promise<boolean> {
const { installNextjs } = await inquirer.prompt([
{
type: "confirm",
name: "installNextjs",
message: `Would you like to create the Next.js storefront? You can also create it later`,
default: false,
},
])
return installNextjs
}
type InstallOptions = {
directoryName: string
abortController?: AbortController
factBoxOptions: FactBoxOptions
verbose?: boolean
}
export async function installNextjsStarter({
directoryName,
abortController,
factBoxOptions,
verbose = false,
}: InstallOptions): Promise<string> {
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Installing Next.js Storefront...",
})
let nextjsDirectory = `${directoryName}-storefront`
if (
fs.existsSync(nextjsDirectory) &&
fs.lstatSync(nextjsDirectory).isDirectory()
) {
// append a random number to the directory name
nextjsDirectory += `-${customAlphabet(
// npm throws an error if the directory name has an uppercase letter
"123456789abcdefghijklmnopqrstuvwxyz",
4
)()}`
}
try {
await execute(
[
`npx create-next-app -e ${NEXTJS_REPO} ${nextjsDirectory}`,
{
signal: abortController?.signal,
env: {
...process.env,
npm_config_yes: "yes",
},
},
],
{ verbose }
)
} catch (e) {
if (isAbortError(e)) {
process.exit()
}
logMessage({
message: `An error occurred while installing Next.js storefront: ${e}`,
type: "error",
})
}
fs.renameSync(
path.join(nextjsDirectory, ".env.template"),
path.join(nextjsDirectory, ".env.local")
)
displayFactBox({
...factBoxOptions,
message: `Installed Next.js Starter successfully in the ${nextjsDirectory} directory.`,
})
return nextjsDirectory
}
type StartOptions = {
directory: string
abortController?: AbortController
verbose?: boolean
}
export function startNextjsStarter({
directory,
abortController,
verbose = false,
}: StartOptions) {
const childProcess = exec(`npm run dev`, {
cwd: directory,
signal: abortController?.signal,
})
if (verbose) {
childProcess.stdout?.pipe(process.stdout)
childProcess.stderr?.pipe(process.stderr)
}
}
@@ -0,0 +1,17 @@
import pg from "pg"
const { Client } = pg
type PostgresConnection = {
user?: string
password?: string
connectionString?: string
database?: string
}
export default async (connect: PostgresConnection) => {
const client = new Client(connect)
await client.connect()
return client
}
@@ -0,0 +1,292 @@
import chalk from "chalk"
import fs from "fs"
import path from "path"
import { Ora } from "ora"
import execute from "./execute.js"
import { EOL } from "os"
import { displayFactBox, FactBoxOptions } from "./facts.js"
import ProcessManager from "./process-manager.js"
import { clearProject } from "./clear-project.js"
import type { Client } from "pg"
type PrepareOptions = {
directory: string
dbConnectionString: string
admin?: {
email: string
}
seed?: boolean
boilerplate?: boolean
spinner: Ora
processManager: ProcessManager
abortController?: AbortController
skipDb?: boolean
migrations?: boolean
onboardingType?: "default" | "nextjs"
nextjsDirectory?: string
client: Client | null
verbose?: boolean
v2?: boolean
}
export default async ({
directory,
dbConnectionString,
admin,
seed,
boilerplate,
spinner,
processManager,
abortController,
skipDb,
migrations,
onboardingType = "default",
nextjsDirectory = "",
client,
verbose = false,
v2 = false,
}: PrepareOptions) => {
// initialize execution options
const execOptions = {
cwd: directory,
signal: abortController?.signal,
}
const npxOptions = {
...execOptions,
env: {
...process.env,
npm_config_yes: "yes",
},
}
const factBoxOptions: FactBoxOptions = {
interval: null,
spinner,
processManager,
message: "",
title: "",
verbose,
}
// initialize the invite token to return
let inviteToken: string | undefined = undefined
if (!skipDb) {
let env = `DATABASE_TYPE=postgres${EOL}DATABASE_URL=${dbConnectionString}${EOL}MEDUSA_ADMIN_ONBOARDING_TYPE=${onboardingType}${EOL}STORE_CORS=http://localhost:8000,http://localhost:7001`
if (v2) {
env += `${EOL}POSTGRES_URL=${dbConnectionString}`
}
if (nextjsDirectory) {
env += `${EOL}MEDUSA_ADMIN_ONBOARDING_NEXTJS_DIRECTORY=${nextjsDirectory}`
}
// add connection string to project
fs.appendFileSync(path.join(directory, `.env`), env)
}
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
spinner,
title: "Installing dependencies...",
processManager,
})
await processManager.runProcess({
process: async () => {
try {
await execute([`yarn`, execOptions], { verbose })
} catch (e) {
// yarn isn't available
// use npm
await execute([`npm install --legacy-peer-deps`, execOptions], {
verbose,
})
}
},
ignoreERESOLVE: true,
})
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
message: "Installed Dependencies",
})
if (!boilerplate) {
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Preparing Project Directory...",
})
// delete files and directories related to onboarding
clearProject(directory)
displayFactBox({
...factBoxOptions,
message: "Prepared Project Directory",
})
}
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Building Project...",
})
await processManager.runProcess({
process: async () => {
try {
await execute([`yarn build`, execOptions], { verbose })
} catch (e) {
// yarn isn't available
// use npm
await execute([`npm run build`, execOptions], { verbose })
}
},
ignoreERESOLVE: true,
})
displayFactBox({ ...factBoxOptions, message: "Project Built" })
if (!skipDb && migrations) {
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Running Migrations...",
})
// run migrations
await processManager.runProcess({
process: async () => {
const proc = await execute(
["npx @medusajs/medusa-cli@latest migrations run", npxOptions],
{ verbose, needOutput: true }
)
if (client) {
// check the migrations table is in the database
// to ensure that migrations ran
let errorOccurred = false
try {
const migrations = await client.query(
`SELECT * FROM "${v2 ? "mikro_orm_migrations" : "migrations"}"`
)
errorOccurred = migrations.rowCount == 0
} catch (e) {
// avoid error thrown if the migrations table
// doesn't exist
errorOccurred = true
}
// ensure that migrations actually ran in case of an uncaught error
if (errorOccurred && (proc.stderr || proc.stdout)) {
throw new Error(
`An error occurred while running migrations: ${
proc.stderr || proc.stdout
}`
)
}
}
},
})
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
message: "Ran Migrations",
})
}
if (admin && !skipDb && migrations && !v2) {
// create admin user
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Creating an admin user...",
})
await processManager.runProcess({
process: async () => {
const proc = await execute(
[
`npx @medusajs/medusa-cli@latest user -e ${admin.email} --invite`,
npxOptions,
],
{ verbose, needOutput: true }
)
// get invite token from stdout
const match = (proc.stdout as string).match(
/Invite token: (?<token>.+)/
)
inviteToken = match?.groups?.token
},
})
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
message: "Created admin user",
})
}
if (!skipDb && migrations) {
if (seed || !boilerplate) {
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Seeding database...",
})
// check if a seed file exists in the project
if (!fs.existsSync(path.join(directory, "data", "seed.json"))) {
spinner
?.warn(
chalk.yellow(
"Seed file was not found in the project. Skipping seeding..."
)
)
.start()
return inviteToken
}
await processManager.runProcess({
process: async () => {
await execute(
[
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
"data",
"seed.json"
)}`,
npxOptions,
],
{ verbose }
)
},
})
displayFactBox({
...factBoxOptions,
message: "Seeded database with demo data",
})
} else if (
fs.existsSync(path.join(directory, "data", "seed-onboarding.json"))
) {
// seed the database with onboarding seed
factBoxOptions.interval = displayFactBox({
...factBoxOptions,
title: "Finish preparation...",
})
await processManager.runProcess({
process: async () => {
await execute(
[
`npx @medusajs/medusa-cli@latest seed --seed-file=${path.join(
"data",
"seed-onboarding.json"
)}`,
npxOptions,
],
{ verbose }
)
},
})
}
displayFactBox({ ...factBoxOptions, message: "Finished Preparation" })
}
return inviteToken
}
@@ -0,0 +1,60 @@
type ProcessOptions = {
process: Function
ignoreERESOLVE?: boolean
}
export default class ProcessManager {
intervals: NodeJS.Timeout[] = []
static MAX_RETRIES = 3
constructor() {
this.onTerminated(() => {
this.intervals.forEach((interval) => {
clearInterval(interval)
})
})
}
onTerminated(fn: () => Promise<void> | void) {
process.on("SIGTERM", () => fn())
process.on("SIGINT", () => fn())
}
addInterval(interval: NodeJS.Timeout) {
this.intervals.push(interval)
}
// when running commands with npx or npm sometimes they
// terminate with EAGAIN error unexpectedly
// this utility function allows retrying the process if
// EAGAIN occurs, or otherwise throw the error that occurs
async runProcess({ process, ignoreERESOLVE }: ProcessOptions) {
let processError = false
let retries = 0
do {
++retries
try {
await process()
} catch (error) {
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error?.code === "EAGAIN"
) {
processError = true
} else if (
ignoreERESOLVE &&
typeof error === "object" &&
error !== null &&
"code" in error &&
error?.code === "ERESOLVE"
) {
// ignore error
} else {
throw error
}
}
} while (processError && retries <= ProcessManager.MAX_RETRIES)
}
}
@@ -0,0 +1,21 @@
import { exec } from "child_process"
type StartOptions = {
directory: string
abortController?: AbortController
}
export default ({ directory, abortController }: StartOptions) => {
const childProcess = exec(`npx @medusajs/medusa-cli@latest develop`, {
cwd: directory,
signal: abortController?.signal,
env: {
...process.env,
OPEN_BROWSER: "false",
npm_config_yes: "yes",
},
})
childProcess.stdout?.pipe(process.stdout)
childProcess.stderr?.pipe(process.stderr)
}