From e102c29af8f48584c7817271554e0b8d9be6214f Mon Sep 17 00:00:00 2001 From: ozakione <29860391+OzakIOne@users.noreply.github.com> Date: Wed, 31 Jul 2024 16:49:35 +0200 Subject: [PATCH] feat: execa --- packages/create-docusaurus/package.json | 1 + packages/create-docusaurus/src/index.ts | 44 ++++++++------- .../package.json | 3 +- packages/docusaurus-utils/package.json | 1 + .../src/__tests__/lastUpdateUtils.test.ts | 2 + packages/docusaurus-utils/src/gitUtils.ts | 56 ++++++++++--------- packages/docusaurus/package.json | 1 + packages/docusaurus/src/commands/deploy.ts | 44 +++++++++------ yarn.lock | 33 ++--------- 9 files changed, 92 insertions(+), 93 deletions(-) diff --git a/packages/create-docusaurus/package.json b/packages/create-docusaurus/package.json index d02a87e1990b..a59c59ea35d9 100755 --- a/packages/create-docusaurus/package.json +++ b/packages/create-docusaurus/package.json @@ -25,6 +25,7 @@ "@docusaurus/logger": "3.4.0", "@docusaurus/utils": "3.4.0", "commander": "^5.1.0", + "execa": "5.1.1", "fs-extra": "^11.1.1", "lodash": "^4.17.21", "prompts": "^2.4.2", diff --git a/packages/create-docusaurus/src/index.ts b/packages/create-docusaurus/src/index.ts index 769fb4885213..26315d65cfdd 100755 --- a/packages/create-docusaurus/src/index.ts +++ b/packages/create-docusaurus/src/index.ts @@ -10,7 +10,7 @@ import {fileURLToPath} from 'url'; import path from 'path'; import _ from 'lodash'; import logger from '@docusaurus/logger'; -import shell from 'shelljs'; +import execa from 'execa'; import prompts, {type Choice} from 'prompts'; import supportsColor from 'supports-color'; import {escapeShellArg, askPreferredLanguage} from '@docusaurus/utils'; @@ -70,9 +70,9 @@ function findPackageManagerFromUserAgent(): PackageManager | undefined { } async function askForPackageManagerChoice(): Promise { - const hasYarn = shell.exec('yarn --version', {silent: true}).code === 0; - const hasPnpm = shell.exec('pnpm --version', {silent: true}).code === 0; - const hasBun = shell.exec('bun --version', {silent: true}).code === 0; + const hasYarn = (await execa.command('yarn --version')).exitCode === 0; + const hasPnpm = (await execa.command('pnpm --version')).exitCode === 0; + const hasBun = (await execa.command('bun --version')).exitCode === 0; if (!hasYarn && !hasPnpm && !hasBun) { return 'npm'; @@ -533,7 +533,7 @@ export default async function init( const gitCloneCommand = `${gitCommand} ${escapeShellArg( source.url, )} ${escapeShellArg(dest)}`; - if (shell.exec(gitCloneCommand).code !== 0) { + if (execa.command(gitCloneCommand).exitCode !== 0) { logger.error`Cloning Git template failed!`; process.exit(1); } @@ -583,24 +583,28 @@ export default async function init( const cdpath = path.relative('.', dest); const pkgManager = await getPackageManager(dest, cliOptions); if (!cliOptions.skipInstall) { - shell.cd(dest); + await execa.command(`cd ${dest}`); logger.info`Installing dependencies with name=${pkgManager}...`; + // ... + if ( - shell.exec( - pkgManager === 'yarn' - ? 'yarn' - : pkgManager === 'bun' - ? 'bun install' - : `${pkgManager} install --color always`, - { - env: { - ...process.env, - // Force coloring the output, since the command is invoked by - // shelljs, which is not an interactive shell - ...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}), + ( + await execa.command( + pkgManager === 'yarn' + ? 'yarn' + : pkgManager === 'bun' + ? 'bun install' + : `${pkgManager} install --color always`, + { + env: { + ...process.env, + // Force coloring the output, since the command is invoked by + // shelljs, which is not an interactive shell + ...(supportsColor.stdout ? {FORCE_COLOR: '1'} : {}), + }, }, - }, - ).code !== 0 + ) + ).exitCode !== 0 ) { logger.error('Dependency installation failed.'); logger.info`The site directory has already been created, and you can retry by typing: diff --git a/packages/docusaurus-plugin-content-docs/package.json b/packages/docusaurus-plugin-content-docs/package.json index 777777e932b8..cb3ec2b6210f 100644 --- a/packages/docusaurus-plugin-content-docs/package.json +++ b/packages/docusaurus-plugin-content-docs/package.json @@ -57,8 +57,7 @@ "@types/js-yaml": "^4.0.5", "@types/picomatch": "^2.3.0", "commander": "^5.1.0", - "picomatch": "^2.3.1", - "shelljs": "^0.8.5" + "picomatch": "^2.3.1" }, "peerDependencies": { "react": "^18.0.0", diff --git a/packages/docusaurus-utils/package.json b/packages/docusaurus-utils/package.json index 5b34b45733e9..f9ef2f8e836e 100644 --- a/packages/docusaurus-utils/package.json +++ b/packages/docusaurus-utils/package.json @@ -22,6 +22,7 @@ "@docusaurus/utils-common": "3.4.0", "@svgr/webpack": "^8.1.0", "escape-string-regexp": "^4.0.0", + "execa": "5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "github-slugger": "^1.5.0", diff --git a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts index 14e907e99462..925f55dcd4f8 100644 --- a/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts +++ b/packages/docusaurus-utils/src/__tests__/lastUpdateUtils.test.ts @@ -10,6 +10,7 @@ import fs from 'fs-extra'; import path from 'path'; import {createTempRepo} from '@testing-utils/git'; import shell from 'shelljs'; +// import execa from 'execa'; import { getGitLastUpdate, LAST_UPDATE_FALLBACK, @@ -69,6 +70,7 @@ describe('getGitLastUpdate', () => { }); it('git does not exist', async () => { + // TODO how to mock execa command ? const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null); const consoleMock = jest .spyOn(console, 'warn') diff --git a/packages/docusaurus-utils/src/gitUtils.ts b/packages/docusaurus-utils/src/gitUtils.ts index 39a3ad754a0c..8f940544fbad 100644 --- a/packages/docusaurus-utils/src/gitUtils.ts +++ b/packages/docusaurus-utils/src/gitUtils.ts @@ -8,9 +8,16 @@ import path from 'path'; import fs from 'fs-extra'; import _ from 'lodash'; -import shell from 'shelljs'; // TODO replace with async-first version - -const realHasGitFn = () => !!shell.which('git'); +import execa from 'execa'; + +const realHasGitFn = async () => { + try { + await execa('git', ['--version']); + return true; + } catch (error) { + return false; + } +}; // The hasGit call is synchronous IO so we memoize it // The user won't install Git in the middle of a build anyway... @@ -123,30 +130,29 @@ export async function getFileCommitDate( file, )}"`; - const result = await new Promise<{ - code: number; - stdout: string; - stderr: string; - }>((resolve) => { - shell.exec( - command, - { - // Setting cwd is important, see: https://github.com/facebook/docusaurus/pull/5048 - cwd: path.dirname(file), - silent: true, - }, - (code, stdout, stderr) => { - resolve({code, stdout, stderr}); - }, - ); - }); - - if (result.code !== 0) { - throw new Error( - `Failed to retrieve the git history for file "${file}" with exit code ${result.code}: ${result.stderr}`, - ); + async function executeCommand(cmd: string, filepath: string) { + try { + const {exitCode, stdout, stderr} = await execa(cmd, { + cwd: path.dirname(filepath), + shell: true, + }); + + if (exitCode !== 0) { + throw new Error( + `Failed to retrieve the git history for file "${file}" with exit code ${exitCode}: ${stderr}`, + ); + } + + return {code: exitCode, stdout, stderr}; + } catch (error) { + console.error('Error executing command:', error); + throw error; + } } + // Usage + const result = await executeCommand(command, file); + // We only parse the output line starting with our "RESULT:" prefix // See why https://github.com/facebook/docusaurus/pull/10022 const regex = includeAuthor diff --git a/packages/docusaurus/package.json b/packages/docusaurus/package.json index c1090dbe2afd..54463e193420 100644 --- a/packages/docusaurus/package.json +++ b/packages/docusaurus/package.json @@ -69,6 +69,7 @@ "escape-html": "^1.0.3", "eta": "^2.2.0", "eval": "^0.1.8", + "execa": "5.1.1", "file-loader": "^6.2.0", "fs-extra": "^11.1.1", "html-minifier-terser": "^7.2.0", diff --git a/packages/docusaurus/src/commands/deploy.ts b/packages/docusaurus/src/commands/deploy.ts index 2e23a8e3efb1..7b17d0822855 100644 --- a/packages/docusaurus/src/commands/deploy.ts +++ b/packages/docusaurus/src/commands/deploy.ts @@ -9,7 +9,7 @@ import fs from 'fs-extra'; import path from 'path'; import os from 'os'; import logger from '@docusaurus/logger'; -import shell from 'shelljs'; +import execa from 'execa'; import {hasSSHProtocol, buildSshUrl, buildHttpsUrl} from '@docusaurus/utils'; import {loadContext, type LoadContextParams} from '../server/site'; import {build} from './build'; @@ -32,8 +32,10 @@ function obfuscateGitPass(str: string) { // for example: https://github.com/facebook/docusaurus/issues/3875 function shellExecLog(cmd: string) { try { - const result = shell.exec(cmd); - logger.info`code=${obfuscateGitPass(cmd)} subdue=${`code: ${result.code}`}`; + const result = execa.command(cmd); + logger.info`code=${obfuscateGitPass( + cmd, + )} subdue=${`code: ${result.exitCode}`}`; return result; } catch (err) { logger.error`code=${obfuscateGitPass(cmd)}`; @@ -61,19 +63,21 @@ This behavior can have SEO impacts and create relative link issues. } logger.info('Deploy command invoked...'); - if (!shell.which('git')) { + try { + await execa.command('git --version'); + } catch (err) { throw new Error('Git not installed or on the PATH!'); } // Source repo is the repo from where the command is invoked - const sourceRepoUrl = shell - .exec('git remote get-url origin', {silent: true}) - .stdout.trim(); + // TODO silent + const {stdout} = await execa.command('git remote get-url origin'); + const sourceRepoUrl = stdout.trim(); // The source branch; defaults to the currently checked out branch const sourceBranch = process.env.CURRENT_BRANCH ?? - shell.exec('git rev-parse --abbrev-ref HEAD', {silent: true}).stdout.trim(); + execa.command('git rev-parse --abbrev-ref HEAD')?.stdout?.toString().trim(); const gitUser = process.env.GIT_USER; @@ -118,8 +122,8 @@ This behavior can have SEO impacts and create relative link issues. const isPullRequest = process.env.CI_PULL_REQUEST ?? process.env.CIRCLE_PULL_REQUEST; if (isPullRequest) { - shell.echo('Skipping deploy on a pull request.'); - shell.exit(0); + await execa.command('echo "Skipping deploy on a pull request."'); + process.exit(0); } // github.io indicates organization repos that deploy via default branch. All @@ -183,7 +187,9 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); // Save the commit hash that triggers publish-gh-pages before checking // out to deployment branch. - const currentCommit = shellExecLog('git rev-parse HEAD').stdout.trim(); + const currentCommit = shellExecLog('git rev-parse HEAD') + ?.stdout?.toString() + .trim(); const runDeploy = async (outputDirectory: string) => { const targetDirectory = cliOptions.targetDir ?? '.'; @@ -191,7 +197,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); const toPath = await fs.mkdtemp( path.join(os.tmpdir(), `${projectName}-${deploymentBranch}`), ); - shell.cd(toPath); + await execa.command(`cd ${toPath}`); // Clones the repo into the temp folder and checks out the target branch. // If the branch doesn't exist, it creates a new one based on the @@ -199,7 +205,7 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); if ( shellExecLog( `git clone --depth 1 --branch ${deploymentBranch} ${deploymentRepoURL} "${toPath}"`, - ).code !== 0 + ).exitCode !== 0 ) { shellExecLog(`git clone --depth 1 ${deploymentRepoURL} "${toPath}"`); shellExecLog(`git checkout -b ${deploymentBranch}`); @@ -232,12 +238,12 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); `Deploy website - based on ${currentCommit}`; const commitResults = shellExecLog(`git commit -m "${commitMessage}"`); if ( - shellExecLog(`git push --force origin ${deploymentBranch}`).code !== 0 + shellExecLog(`git push --force origin ${deploymentBranch}`).exitCode !== 0 ) { throw new Error( 'Running "git push" command failed. Does the GitHub user account you are using have push access to the repository?', ); - } else if (commitResults.code === 0) { + } else if (commitResults.exitCode === 0) { // The commit might return a non-zero value when site is up to date. let websiteURL = ''; if (githubHost === 'github.com') { @@ -248,8 +254,12 @@ You can also set the deploymentBranch property in docusaurus.config.js .`); // GitHub enterprise hosting. websiteURL = `https://${githubHost}/pages/${organizationName}/${projectName}/`; } - shell.echo(`Website is live at "${websiteURL}".`); - shell.exit(0); + try { + await execa.command(`echo "Website is live at ${websiteURL}."`); + process.exit(0); + } catch (err) { + throw new Error(`Failed to execute command: ${err}`); + } } }; diff --git a/yarn.lock b/yarn.lock index 461971db46db..f25897322ae3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7410,7 +7410,7 @@ execa@5.0.0: signal-exit "^3.0.3" strip-final-newline "^2.0.0" -execa@^5.0.0: +execa@5.1.1, execa@^5.0.0: version "5.1.1" resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== @@ -15245,16 +15245,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -15353,14 +15344,7 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17025,7 +17009,7 @@ workbox-window@7.0.0, workbox-window@^7.0.0: "@types/trusted-types" "^2.0.2" workbox-core "7.0.0" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -17043,15 +17027,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"