From 21ec74da7f93fc13e253d7b35ddeddc23422a6c1 Mon Sep 17 00:00:00 2001 From: awkweb Date: Wed, 11 Dec 2024 20:46:49 -0500 Subject: [PATCH] refactor(cli): drop execa (#4457) * wip: exec refactor * refactor(cli): drop execa * chore: tweaks * fix: watch mode * refactor: more --- .changeset/chatty-cows-kiss.md | 5 +++ packages/cli/package.json | 1 - packages/cli/src/cli.ts | 11 ++++-- packages/cli/src/commands/generate.test.ts | 2 +- packages/cli/src/commands/generate.ts | 4 +- packages/cli/src/plugins/foundry.test.ts | 6 +-- packages/cli/src/plugins/foundry.ts | 40 +++++++++++++------ packages/cli/src/plugins/hardhat.ts | 16 ++++++-- packages/cli/src/utils/packages.ts | 36 +++++++++-------- packages/cli/test/utils.ts | 25 ++++++------ packages/create-wagmi/package.json | 3 +- packages/create-wagmi/src/cli.test.ts | 45 +++++++++++++--------- playgrounds/vite-react/.gitignore | 2 + playgrounds/vite-react/package.json | 1 + playgrounds/vite-react/wagmi.config.ts | 17 ++++++++ pnpm-lock.yaml | 9 ++--- 16 files changed, 145 insertions(+), 78 deletions(-) create mode 100644 .changeset/chatty-cows-kiss.md create mode 100644 playgrounds/vite-react/wagmi.config.ts diff --git a/.changeset/chatty-cows-kiss.md b/.changeset/chatty-cows-kiss.md new file mode 100644 index 0000000000..59c775f977 --- /dev/null +++ b/.changeset/chatty-cows-kiss.md @@ -0,0 +1,5 @@ +--- +"@wagmi/cli": patch +--- + +Removed internal dependency. diff --git a/packages/cli/package.json b/packages/cli/package.json index 48e062315f..92aa81c363 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -73,7 +73,6 @@ "dotenv-expand": "^10.0.0", "esbuild": "^0.19.0", "escalade": "3.2.0", - "execa": "^8.0.1", "fdir": "^6.1.1", "nanospinner": "1.2.2", "pathe": "^1.1.2", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 92e07206f4..551543bb2b 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -14,14 +14,20 @@ cli .option('-r, --root ', '[string] root path to resolve config from') .option('-w, --watch', '[boolean] watch for changes') .example((name) => `${name} generate`) - .action(async (options: Generate) => await generate(options)) + .action(async (options: Generate) => { + await generate(options) + if (!options.watch) process.exit(0) + }) cli .command('init', 'create configuration file') .option('-c, --config ', '[string] path to config file') .option('-r, --root ', '[string] root path to resolve config from') .example((name) => `${name} init`) - .action(async (options: Init) => await init(options)) + .action(async (options: Init) => { + await init(options) + process.exit(0) + }) cli.help() cli.version(version) @@ -40,7 +46,6 @@ void (async () => { } else throw new Error(`Unknown command: ${cli.args.join(' ')}`) } await cli.runMatchedCommand() - process.exit(0) } catch (error) { logger.error(`\n${(error as Error).message}`) process.exit(1) diff --git a/packages/cli/src/commands/generate.test.ts b/packages/cli/src/commands/generate.test.ts index 79c71d6fe6..91e4265216 100644 --- a/packages/cli/src/commands/generate.test.ts +++ b/packages/cli/src/commands/generate.test.ts @@ -1,8 +1,8 @@ +import { readFile } from 'node:fs/promises' import dedent from 'dedent' import { resolve } from 'pathe' import { afterEach, beforeEach, expect, test, vi } from 'vitest' -import { readFile } from 'node:fs/promises' import { createFixture, typecheck, watchConsole } from '../../test/utils.js' import { generate } from './generate.js' diff --git a/packages/cli/src/commands/generate.ts b/packages/cli/src/commands/generate.ts index 6e70b87bba..5e1f5a2a1a 100644 --- a/packages/cli/src/commands/generate.ts +++ b/packages/cli/src/commands/generate.ts @@ -53,12 +53,12 @@ export async function generate(options: Generate = {}) { type Watcher = FSWatcher & { config?: Watch } const watchers: Watcher[] = [] const watchWriteDelay = 100 - const watchOptions: ChokidarOptions = { + const watchOptions = { atomic: true, // awaitWriteFinish: true, ignoreInitial: true, persistent: true, - } + } satisfies ChokidarOptions const outNames = new Set() const isArrayConfig = Array.isArray(resolvedConfigs) diff --git a/packages/cli/src/plugins/foundry.test.ts b/packages/cli/src/plugins/foundry.test.ts index 5ff95c0b39..75e5ec73ee 100644 --- a/packages/cli/src/plugins/foundry.test.ts +++ b/packages/cli/src/plugins/foundry.test.ts @@ -47,8 +47,8 @@ test('validates without project', async () => { await expect(foundry().validate?.()).resolves.toBeUndefined() }) -test('contracts', () => { - expect( +test('contracts', async () => { + await expect( foundry({ project: resolve(__dirname, '__fixtures__/foundry/'), exclude: ['Foo.sol/**'], @@ -103,7 +103,7 @@ test('contracts without project', async () => { const spy = vi.spyOn(process, 'cwd') spy.mockImplementation(() => dir) - expect( + await expect( foundry({ exclude: ['Foo.sol/**'], }).contracts?.(), diff --git a/packages/cli/src/plugins/foundry.ts b/packages/cli/src/plugins/foundry.ts index cbd09c6821..dab307a58f 100644 --- a/packages/cli/src/plugins/foundry.ts +++ b/packages/cli/src/plugins/foundry.ts @@ -1,7 +1,7 @@ +import { execSync, spawn, spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' import dedent from 'dedent' -import { execa, execaCommandSync } from 'execa' import { fdir } from 'fdir' import { basename, extname, join, resolve } from 'pathe' import pc from 'picocolors' @@ -153,12 +153,19 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { src: 'src', } try { - foundryConfig = FoundryConfigSchema.parse( - JSON.parse( - execaCommandSync(`${forgeExecutable} config --json --root ${project}`) - .stdout, - ), + const result = spawnSync( + forgeExecutable, + ['config', '--json', '--root', project], + { + encoding: 'utf-8', + shell: true, + }, ) + if (result.error) throw result.error + if (result.status !== 0) + throw new Error(`Failed with code ${result.status}`) + if (result.signal) throw new Error('Process terminated by signal') + foundryConfig = FoundryConfigSchema.parse(JSON.parse(result.stdout)) } catch { } finally { foundryConfig = { @@ -171,8 +178,16 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { return { async contracts() { - if (clean) await execa(forgeExecutable, ['clean', '--root', project]) - if (build) await execa(forgeExecutable, ['build', '--root', project]) + if (clean) + execSync(`${forgeExecutable} clean --root ${project}`, { + encoding: 'utf-8', + stdio: 'pipe', + }) + if (build) + execSync(`${forgeExecutable} build --root ${project}`, { + encoding: 'utf-8', + stdio: 'pipe', + }) if (!existsSync(artifactsDirectory)) throw new Error('Artifacts not found.') @@ -194,7 +209,10 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { // Ensure forge is installed if (clean || build || rebuild) try { - await execa(forgeExecutable, ['--version']) + execSync(`${forgeExecutable} --version`, { + encoding: 'utf-8', + stdio: 'pipe', + }) } catch (_error) { throw new Error(dedent` forge must be installed to use Foundry plugin. @@ -210,7 +228,7 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { project, )}`, ) - const subprocess = execa(forgeExecutable, [ + const subprocess = spawn(forgeExecutable, [ 'build', '--watch', '--root', @@ -223,7 +241,7 @@ export function foundry(config: FoundryConfig = {}): FoundryResult { process.once('SIGINT', shutdown) process.once('SIGTERM', shutdown) function shutdown() { - subprocess?.cancel() + subprocess?.kill() } } : undefined, diff --git a/packages/cli/src/plugins/hardhat.ts b/packages/cli/src/plugins/hardhat.ts index e1bc211f56..a4feb6efdf 100644 --- a/packages/cli/src/plugins/hardhat.ts +++ b/packages/cli/src/plugins/hardhat.ts @@ -1,6 +1,6 @@ +import { execSync, spawn } from 'node:child_process' import { existsSync } from 'node:fs' import { readFile } from 'node:fs/promises' -import { execa } from 'execa' import { fdir } from 'fdir' import { basename, extname, join, resolve } from 'pathe' import pc from 'picocolors' @@ -116,7 +116,11 @@ export function hardhat(config: HardhatConfig): HardhatResult { const [command, ...options] = ( typeof clean === 'boolean' ? `${packageManager} hardhat clean` : clean ).split(' ') - await execa(command!, options, { cwd: project }) + execSync(`${command!} ${options.join(' ')}`, { + cwd: project, + encoding: 'utf-8', + stdio: 'pipe', + }) } if (build) { const packageManager = await getPackageManager(true) @@ -125,7 +129,11 @@ export function hardhat(config: HardhatConfig): HardhatResult { ? `${packageManager} hardhat compile` : build ).split(' ') - await execa(command!, options, { cwd: project }) + execSync(`${command!} ${options.join(' ')}`, { + cwd: project, + encoding: 'utf-8', + stdio: 'pipe', + }) } if (!existsSync(artifactsDirectory)) throw new Error('Artifacts not found.') @@ -180,7 +188,7 @@ export function hardhat(config: HardhatConfig): HardhatResult { logger.log( `${pc.blue('Hardhat')} Detected ${event} at ${basename(path)}`, ) - const subprocess = execa(command!, options, { + const subprocess = spawn(command!, options, { cwd: project, }) subprocess.stdout?.on('data', (data) => { diff --git a/packages/cli/src/utils/packages.ts b/packages/cli/src/utils/packages.ts index 91fd0a5a72..e55eeaf0ea 100644 --- a/packages/cli/src/utils/packages.ts +++ b/packages/cli/src/utils/packages.ts @@ -1,6 +1,6 @@ +import { execSync } from 'node:child_process' import { promises as fs } from 'node:fs' import { resolve } from 'node:path' -import { execa } from 'execa' export async function getIsPackageInstalled(parameters: { packageName: string @@ -20,12 +20,16 @@ export async function getIsPackageInstalled(parameters: { } })() - const { stdout } = await execa(packageManager, command, { cwd }) + const result = execSync(`${packageManager} ${command.join(' ')}`, { + cwd, + encoding: 'utf-8', + stdio: 'pipe', + }) // For Bun, we need to check if the package name is in the output - if (packageManager === 'bun') return stdout.includes(packageName) + if (packageManager === 'bun') return result.includes(packageName) - return stdout !== '' + return result !== '' } catch (_error) { return false } @@ -69,21 +73,21 @@ async function detect( const cache = new Map() -function hasGlobalInstallation(pm: PackageManager): Promise { +function hasGlobalInstallation(pm: PackageManager): boolean { const key = `has_global_${pm}` - if (cache.has(key)) { - return Promise.resolve(cache.get(key)) - } + if (cache.has(key)) return cache.get(key) - return execa(pm, ['--version']) - .then((res) => { - return /^\d+.\d+.\d+$/.test(res.stdout) - }) - .then((value) => { - cache.set(key, value) - return value + try { + const result = execSync(`${pm} --version`, { + encoding: 'utf-8', + stdio: 'pipe', }) - .catch(() => false) + const isGlobal = /^\d+.\d+.\d+$/.test(result) + cache.set(key, isGlobal) + return isGlobal + } catch { + return false + } } function getTypeofLockFile(cwd = '.'): Promise { diff --git a/packages/cli/test/utils.ts b/packages/cli/test/utils.ts index 0efbb969fe..6c63bd725e 100644 --- a/packages/cli/test/utils.ts +++ b/packages/cli/test/utils.ts @@ -1,5 +1,5 @@ +import { spawnSync } from 'node:child_process' import { cp, mkdir, symlink, writeFile } from 'node:fs/promises' -import { execa } from 'execa' import fixtures from 'fixturez' import { http, HttpResponse } from 'msw' import * as path from 'pathe' @@ -144,16 +144,19 @@ export function watchConsole() { export async function typecheck(project: string) { try { - const res = await execa('tsc', [ - '--noEmit', - '--target', - 'es2021', - '--pretty', - 'false', - '-p', - project, - ]) - return res.stdout + const result = spawnSync( + 'tsc', + ['--noEmit', '--target', 'es2021', '--pretty', 'false', '-p', project], + { + encoding: 'utf-8', + stdio: 'pipe', + }, + ) + if (result.error) throw result.error + if (result.status !== 0) + throw new Error(`Failed with code ${result.status}`) + if (result.signal) throw new Error('Process terminated by signal') + return result.stdout } catch (error) { throw new Error( (error as Error).message.replaceAll( diff --git a/packages/create-wagmi/package.json b/packages/create-wagmi/package.json index 44eab77347..b594422ae9 100644 --- a/packages/create-wagmi/package.json +++ b/packages/create-wagmi/package.json @@ -41,8 +41,7 @@ "devDependencies": { "@types/cross-spawn": "^6.0.6", "@types/node": "^20.12.10", - "@types/prompts": "^2.4.9", - "execa": "^8.0.1" + "@types/prompts": "^2.4.9" }, "contributors": ["awkweb.eth ", "jxom.eth "], "funding": "https://github.com/sponsors/wevm", diff --git a/packages/create-wagmi/src/cli.test.ts b/packages/create-wagmi/src/cli.test.ts index 94a68b39fd..4d6915a6f3 100644 --- a/packages/create-wagmi/src/cli.test.ts +++ b/packages/create-wagmi/src/cli.test.ts @@ -1,8 +1,10 @@ +import { + type ExecSyncOptionsWithStringEncoding, + execSync, +} from 'node:child_process' import { mkdirSync, readdirSync, writeFileSync } from 'node:fs' import { rm } from 'node:fs/promises' import { join } from 'node:path' -import type { ExecaSyncReturnValue, SyncOptions } from 'execa' -import { execaCommandSync } from 'execa' import pc from 'picocolors' import { afterEach, beforeAll, expect, test } from 'vitest' @@ -13,8 +15,11 @@ const cliPath = join(__dirname, '../dist/esm/cli.js') const projectName = 'test-app' const genPath = join(__dirname, projectName) -function run(args: string[], options: SyncOptions = {}): ExecaSyncReturnValue { - return execaCommandSync(`node ${cliPath} ${args.join(' ')}`, options) +function run( + args: string[], + options: ExecSyncOptionsWithStringEncoding = { encoding: 'utf8' }, +): string { + return execSync(`node ${cliPath} ${args.join(' ')}`, options) } function createNonEmptyDir() { @@ -24,7 +29,7 @@ function createNonEmptyDir() { } beforeAll(async () => { - execaCommandSync('pnpm --filter create-wagmi build') + execSync('pnpm --filter create-wagmi build') await rm(genPath, { recursive: true, force: true }) }) afterEach(async () => { @@ -32,28 +37,28 @@ afterEach(async () => { }) test('prompts for the project name if none supplied', () => { - const { stdout } = run([]) + const stdout = run([]) expect(stdout).toContain('Project name:') }) test('prompts for the framework if none supplied when target dir is current directory', () => { mkdirSync(genPath) - const { stdout } = run(['.'], { cwd: genPath }) + const stdout = run(['.'], { cwd: genPath, encoding: 'utf8' }) expect(stdout).toContain('Select a framework:') }) test('prompts for the framework if none supplied', () => { - const { stdout } = run([projectName]) + const stdout = run([projectName]) expect(stdout).toContain('Select a framework:') }) test('prompts for the framework on not supplying a value for --template', () => { - const { stdout } = run([projectName, '--template']) + const stdout = run([projectName, '--template']) expect(stdout).toContain('Select a framework:') }) test('prompts for the framework on supplying an invalid template', () => { - const { stdout } = run([projectName, '--template', 'unknown']) + const stdout = run([projectName, '--template', 'unknown']) expect(stdout).toContain( `"unknown" isn't a valid template. Please choose from below:`, ) @@ -61,13 +66,13 @@ test('prompts for the framework on supplying an invalid template', () => { test('asks to overwrite non-empty target directory', () => { createNonEmptyDir() - const { stdout } = run([projectName], { cwd: __dirname }) + const stdout = run([projectName], { cwd: __dirname, encoding: 'utf8' }) expect(stdout).toContain(`Target directory "${projectName}" is not empty.`) }) test('asks to overwrite non-empty current directory', () => { createNonEmptyDir() - const { stdout } = run(['.'], { cwd: genPath }) + const stdout = run(['.'], { cwd: genPath, encoding: 'utf8' }) expect(stdout).toContain('Current directory is not empty.') }) @@ -84,8 +89,9 @@ const templateFiles = readdirSync( test('successfully scaffolds a project based on vite-react starter template', () => { mkdirSync(genPath, { recursive: true }) - const { stdout } = run([projectName, '--template', 'vite-react'], { + const stdout = run([projectName, '--template', 'vite-react'], { cwd: __dirname, + encoding: 'utf8', }) const generatedFiles = readdirSync(genPath).sort() @@ -95,8 +101,9 @@ test('successfully scaffolds a project based on vite-react starter template', () test('works with the -t alias', () => { mkdirSync(genPath, { recursive: true }) - const { stdout } = run([projectName, '-t', 'vite-react'], { + const stdout = run([projectName, '-t', 'vite-react'], { cwd: __dirname, + encoding: 'utf8', }) const generatedFiles = readdirSync(genPath).sort() @@ -106,15 +113,16 @@ test('works with the -t alias', () => { test('uses different package manager', () => { mkdirSync(genPath, { recursive: true }) - const { stdout } = run([projectName, '--bun', '-t', 'vite-react'], { + const stdout = run([projectName, '--bun', '-t', 'vite-react'], { cwd: __dirname, + encoding: 'utf8', }) expect(stdout).toContain('bun install') }) test('shows help', () => { - const { stdout } = run(['--help']) + const stdout = run(['--help']) expect( stdout .replace(version, 'x.y.z') @@ -132,11 +140,12 @@ test('shows help', () => { --pnpm Use pnpm as your package manager --yarn Use yarn as your package manager -h, --help Display this message - -v, --version Display version number " + -v, --version Display version number + " `) }) test('shows version', () => { - const { stdout } = run(['--version']) + const stdout = run(['--version']) expect(stdout).toContain(`create-wagmi/${version} `) }) diff --git a/playgrounds/vite-react/.gitignore b/playgrounds/vite-react/.gitignore index a547bf36d8..1b226cc615 100644 --- a/playgrounds/vite-react/.gitignore +++ b/playgrounds/vite-react/.gitignore @@ -1,3 +1,5 @@ +src/generated.ts + # Logs logs *.log diff --git a/playgrounds/vite-react/package.json b/playgrounds/vite-react/package.json index b6c19deb24..918ad0cf85 100644 --- a/playgrounds/vite-react/package.json +++ b/playgrounds/vite-react/package.json @@ -22,6 +22,7 @@ "@types/react": ">=18.3.1", "@types/react-dom": ">=18.3.0", "@vitejs/plugin-react": "^4.2.1", + "@wagmi/cli": "workspace:*", "buffer": "^6.0.3", "vite": "^5.2.11" } diff --git a/playgrounds/vite-react/wagmi.config.ts b/playgrounds/vite-react/wagmi.config.ts new file mode 100644 index 0000000000..d524220b09 --- /dev/null +++ b/playgrounds/vite-react/wagmi.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@wagmi/cli' +import { foundry, hardhat } from '@wagmi/cli/plugins' + +export default defineConfig({ + out: 'src/generated.ts', + contracts: [], + plugins: [ + foundry({ + namePrefix: 'foundry', + project: '../../packages/cli/src/plugins/__fixtures__/foundry', + }), + hardhat({ + namePrefix: 'hardhat', + project: '../../packages/cli/src/plugins/__fixtures__/hardhat', + }), + ], +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 277fad4368..892fa8dddc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ importers: escalade: specifier: 3.2.0 version: 3.2.0 - execa: - specifier: ^8.0.1 - version: 8.0.1 fdir: specifier: ^6.1.1 version: 6.1.1(picomatch@3.0.1) @@ -213,9 +210,6 @@ importers: '@types/prompts': specifier: ^2.4.9 version: 2.4.9 - execa: - specifier: ^8.0.1 - version: 8.0.1 packages/react: dependencies: @@ -492,6 +486,9 @@ importers: '@vitejs/plugin-react': specifier: ^4.2.1 version: 4.2.1(vite@5.2.11(@types/node@20.12.10)(terser@5.31.0)) + '@wagmi/cli': + specifier: workspace:* + version: link:../../packages/cli buffer: specifier: ^6.0.3 version: 6.0.3