diff --git a/.changeset/funny-pears-deny.md b/.changeset/funny-pears-deny.md new file mode 100644 index 000000000..9fb91a6d0 --- /dev/null +++ b/.changeset/funny-pears-deny.md @@ -0,0 +1,7 @@ +--- +"create-lz-oapp": patch +"@layerzerolabs/oapp-example": patch +"@layerzerolabs/oft-example": patch +--- + +Improve error handling and introduce node engine specifiers diff --git a/examples/oapp/.nvmrc b/examples/oapp/.nvmrc new file mode 100644 index 000000000..b1215e876 --- /dev/null +++ b/examples/oapp/.nvmrc @@ -0,0 +1 @@ +v18.16.0 \ No newline at end of file diff --git a/examples/oapp/package.json b/examples/oapp/package.json index 03392dd0d..0e1a7ac75 100644 --- a/examples/oapp/package.json +++ b/examples/oapp/package.json @@ -47,5 +47,8 @@ "solidity-bytes-utils": "^0.8.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.16.0" } } diff --git a/examples/oft/.nvmrc b/examples/oft/.nvmrc new file mode 100644 index 000000000..b1215e876 --- /dev/null +++ b/examples/oft/.nvmrc @@ -0,0 +1 @@ +v18.16.0 \ No newline at end of file diff --git a/examples/oft/package.json b/examples/oft/package.json index 7a3ec48c1..c5f19fbb4 100644 --- a/examples/oft/package.json +++ b/examples/oft/package.json @@ -47,5 +47,8 @@ "solidity-bytes-utils": "^0.8.2", "ts-node": "^10.9.2", "typescript": "^5.3.3" + }, + "engines": { + "node": ">=18.16.0" } } diff --git a/package.json b/package.json index f0608f419..dfc26632f 100644 --- a/package.json +++ b/package.json @@ -53,5 +53,8 @@ "prettier-plugin-solidity": "^1.3.1", "turbo": "1.11.0" }, - "packageManager": "pnpm@8.14.0" + "packageManager": "pnpm@8.14.0", + "engines": { + "node": ">=18.16.0" + } } diff --git a/packages/create-lz-oapp/src/components/error.tsx b/packages/create-lz-oapp/src/components/error.tsx index ef9d78021..955086a19 100644 --- a/packages/create-lz-oapp/src/components/error.tsx +++ b/packages/create-lz-oapp/src/components/error.tsx @@ -1,12 +1,13 @@ import React from "react"; import type { Config } from "@/types"; -import { Box, Text } from "ink"; +import { Box, Newline, Text } from "ink"; import { BadGitRefError, DestinationNotEmptyError, DownloadError, MissingGitRefError, } from "@/utilities/cloning"; +import { InstallationError } from "@/utilities/installation"; interface ErrorMessageProps { config: Config; @@ -58,6 +59,37 @@ export const ErrorMessage: React.FC = ({ ); + case error instanceof InstallationError: + return ( + + + There was a problem installing NPM dependencies: + + + + {error.stdout} + + + To try again: + + + # Navigate to your project + cd {config.destination} + + # Reattempt the installation + + {config.packageManager.executable}{" "} + {config.packageManager.args.join(" ")} + + + + ); + case error instanceof Error: return ; diff --git a/packages/create-lz-oapp/src/index.tsx b/packages/create-lz-oapp/src/index.tsx index faae68c27..e0c05ac77 100644 --- a/packages/create-lz-oapp/src/index.tsx +++ b/packages/create-lz-oapp/src/index.tsx @@ -4,13 +4,18 @@ import { Command } from "commander"; import { promptForConfig } from "@/utilities/prompts"; import { ConfigSummary } from "@/components/config"; import { Setup } from "@/components/setup"; -import { promptToContinue } from "@layerzerolabs/io-devtools"; +import { + LogLevel, + promptToContinue, + setDefaultLogLevel, +} from "@layerzerolabs/io-devtools"; import { printLogo } from "@layerzerolabs/io-devtools/swag"; import { version } from "../package.json"; import { ciOption, destinationOption, exampleOption, + logLevelOption, packageManagerOption, } from "./options"; import type { Config, Example, PackageManager } from "./types"; @@ -20,6 +25,7 @@ interface Args { ci?: boolean; destination?: string; example?: Example; + logLevel?: LogLevel; packageManager: PackageManager; } @@ -29,13 +35,17 @@ new Command("create-lz-oapp") .addOption(ciOption) .addOption(destinationOption) .addOption(exampleOption) + .addOption(logLevelOption) .addOption(packageManagerOption) .action(async (args: Args) => { printLogo(); // We'll provide a CI mode - a non-interctaive mode in which all input is taken // from the CLI arguments and if something is missing, an error is thrown - const { ci } = args; + const { ci, logLevel = "info" } = args; + + // We'll set a default log level for any loggers created past this point + setDefaultLogLevel(logLevel); // First we get the config const config = ci diff --git a/packages/create-lz-oapp/src/options.ts b/packages/create-lz-oapp/src/options.ts index fa4784f83..576cd0ae5 100644 --- a/packages/create-lz-oapp/src/options.ts +++ b/packages/create-lz-oapp/src/options.ts @@ -1,6 +1,6 @@ import { InvalidOptionArgumentError, Option } from 'commander' import { AVAILABLE_PACKAGE_MANAGERS, EXAMPLES } from './config' -import { isDirectory, isFile } from '@layerzerolabs/io-devtools' +import { LogLevel, isDirectory, isFile } from '@layerzerolabs/io-devtools' import { resolve } from 'path' export const packageManagerOption = new Option('-p,--package-manager ', 'Node package manager to use') @@ -38,3 +38,15 @@ export const destinationOption = new Option('-d,--destination ', 'Project }) export const ciOption = new Option('--ci', 'Run in CI (non-interactive) mode').default(false) + +export const logLevelOption = new Option('--log-level ', 'Log level') + .choices([ + LogLevel.error, + LogLevel.warn, + LogLevel.info, + LogLevel.http, + LogLevel.verbose, + LogLevel.debug, + LogLevel.silly, + ]) + .default(LogLevel.info) diff --git a/packages/create-lz-oapp/src/utilities/cloning.ts b/packages/create-lz-oapp/src/utilities/cloning.ts index 53e243825..f0ed55d76 100644 --- a/packages/create-lz-oapp/src/utilities/cloning.ts +++ b/packages/create-lz-oapp/src/utilities/cloning.ts @@ -1,4 +1,5 @@ import { Config, Example } from '@/types' +import { createModuleLogger } from '@layerzerolabs/io-devtools' import { rm } from 'fs/promises' import { resolve } from 'path' import tiged from 'tiged' @@ -20,7 +21,11 @@ export const createExampleGitURL = (example: Example): string => { } export const cloneExample = async ({ example, destination }: Config) => { + const logger = createModuleLogger('cloning') + const url = createExampleGitURL(example) + logger.verbose(`Cloning example from ${url} to ${destination}`) + const emitter = tiged(url, { disableCache: true, mode: 'git', @@ -31,6 +36,9 @@ export const cloneExample = async ({ example, destination }: Config) => { // First we clone the whole proejct await emitter.clone(destination) + logger.verbose(`Cloned example from ${url} to ${destination}`) + logger.verbose(`Cleaning up`) + // Then we cleanup what we don't want to be included await cleanupExample(destination) } catch (error: unknown) { diff --git a/packages/create-lz-oapp/src/utilities/installation.ts b/packages/create-lz-oapp/src/utilities/installation.ts index bc64cd861..295d89cc6 100644 --- a/packages/create-lz-oapp/src/utilities/installation.ts +++ b/packages/create-lz-oapp/src/utilities/installation.ts @@ -1,9 +1,22 @@ import type { Config, PackageManager } from '@/types' +import { createModuleLogger } from '@layerzerolabs/io-devtools' import { spawn } from 'child_process' import which from 'which' export const installDependencies = (config: Config) => new Promise((resolve, reject) => { + const logger = createModuleLogger('installation') + + // We'll store combined stdout and stderr in this variable + const std: string[] = [] + + // This function will handle stdout/stderr streams from the child process + const handleStd = (chunk: string) => { + std.push(chunk) + + logger.verbose(chunk) + } + /** * Spawn the installation process. */ @@ -19,16 +32,39 @@ export const installDependencies = (config: Config) => }, }) + child.stdout.setEncoding('utf8') + child.stderr.setEncoding('utf8') + child.stdout.on('data', handleStd) + child.stderr.on('data', handleStd) + child.on('close', (code) => { - if (code !== 0) { - reject( - new Error( - `Failed to install dependencies: ${config.packageManager.label} install exited with code ${code}` - ) - ) - } else resolve() + switch (code) { + // The null case happens when the script receives a sigterm signall + // (i.e. is cancelled by the user) + case null: + return reject(new Error(`Failed to install dependencies: Installation interrupted`)) + + // 0 exit code means success + case 0: + return resolve() + + // And any other non-zero exit code means an error + default: + return reject(new InstallationError(config.packageManager, code, std.join(''))) + } }) }) export const isPackageManagerAvailable = ({ executable }: PackageManager): boolean => !!which.sync(executable, { nothrow: true }) + +export class InstallationError extends Error { + constructor( + public readonly packageManager: PackageManager, + public readonly exitCode: number, + public readonly stdout: string, + message: string = `Failed to install dependencies: ${packageManager.label} exited with code ${exitCode}` + ) { + super(message) + } +}