mirror of
https://github.com/facebook/docusaurus.git
synced 2025-04-28 09:47:48 +02:00
refactor: replace unmaintained shelljs dependency by execa (#10358)
Co-authored-by: sebastien <lorber.sebastien@gmail.com>
This commit is contained in:
parent
a6ef3897e0
commit
7f4a37949e
10 changed files with 139 additions and 85 deletions
|
@ -25,11 +25,11 @@
|
|||
"@docusaurus/logger": "3.7.0",
|
||||
"@docusaurus/utils": "3.7.0",
|
||||
"commander": "^5.1.0",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"lodash": "^4.17.21",
|
||||
"prompts": "^2.4.2",
|
||||
"semver": "^7.5.4",
|
||||
"shelljs": "^0.8.5",
|
||||
"supports-color": "^9.4.0",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
|
|
|
@ -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<PackageManager> {
|
||||
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);
|
||||
process.chdir(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:
|
||||
|
|
|
@ -58,8 +58,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 || ^19.0.0",
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"@docusaurus/types": "3.7.0",
|
||||
"@docusaurus/utils-common": "3.7.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",
|
||||
|
@ -33,7 +34,6 @@
|
|||
"micromatch": "^4.0.5",
|
||||
"prompts": "^2.4.2",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"shelljs": "^0.8.5",
|
||||
"tslib": "^2.6.0",
|
||||
"url-loader": "^4.1.1",
|
||||
"utility-types": "^3.10.0",
|
||||
|
|
|
@ -9,12 +9,13 @@ import {jest} from '@jest/globals';
|
|||
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,
|
||||
readLastUpdateData,
|
||||
} from '@docusaurus/utils';
|
||||
} from '../lastUpdateUtils';
|
||||
|
||||
describe('getGitLastUpdate', () => {
|
||||
const {repoDir} = createTempRepo();
|
||||
|
@ -69,7 +70,10 @@ describe('getGitLastUpdate', () => {
|
|||
});
|
||||
|
||||
it('git does not exist', async () => {
|
||||
const mock = jest.spyOn(shell, 'which').mockImplementationOnce(() => null);
|
||||
const mock = jest.spyOn(execa, 'sync').mockImplementationOnce(() => {
|
||||
throw new Error('Git does not exist');
|
||||
});
|
||||
|
||||
const consoleMock = jest
|
||||
.spyOn(console, 'warn')
|
||||
.mockImplementation(() => {});
|
||||
|
|
|
@ -8,9 +8,15 @@
|
|||
import path from 'path';
|
||||
import fs from 'fs-extra';
|
||||
import _ from 'lodash';
|
||||
import shell from 'shelljs'; // TODO replace with async-first version
|
||||
import execa from 'execa';
|
||||
|
||||
const realHasGitFn = () => !!shell.which('git');
|
||||
const realHasGitFn = () => {
|
||||
try {
|
||||
return execa.sync('git', ['--version']).exitCode === 0;
|
||||
} 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,27 +129,13 @@ 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});
|
||||
},
|
||||
);
|
||||
const result = await execa(command, {
|
||||
cwd: path.dirname(file),
|
||||
shell: true,
|
||||
});
|
||||
|
||||
if (result.code !== 0) {
|
||||
if (result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Failed to retrieve the git history for file "${file}" with exit code ${result.code}: ${result.stderr}`,
|
||||
`Failed to retrieve the git history for file "${file}" with exit code ${result.exitCode}: ${result.stderr}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import {createRequire} from 'module';
|
||||
import shell from 'shelljs';
|
||||
import execa from 'execa';
|
||||
import {logger} from '@docusaurus/logger';
|
||||
import semver from 'semver';
|
||||
import updateNotifier from 'update-notifier';
|
||||
|
@ -111,8 +111,8 @@ export default async function beforeCli() {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
const yarnVersionResult = shell.exec('yarn --version', {silent: true});
|
||||
if (yarnVersionResult?.code === 0) {
|
||||
const yarnVersionResult = await execa.command('yarn --version');
|
||||
if (yarnVersionResult.exitCode === 0) {
|
||||
const majorVersion = parseInt(
|
||||
yarnVersionResult.stdout?.trim().split('.')[0] ?? '',
|
||||
10,
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"escape-html": "^1.0.3",
|
||||
"eta": "^2.2.0",
|
||||
"eval": "^0.1.8",
|
||||
"execa": "5.1.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"html-tags": "^3.3.1",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
|
@ -68,7 +69,6 @@
|
|||
"react-router-dom": "^5.3.4",
|
||||
"semver": "^7.5.4",
|
||||
"serve-handler": "^6.1.6",
|
||||
"shelljs": "^0.8.5",
|
||||
"tinypool": "^1.0.2",
|
||||
"tslib": "^2.6.0",
|
||||
"update-notifier": "^6.0.2",
|
||||
|
|
|
@ -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/build';
|
||||
|
@ -26,19 +26,53 @@ function obfuscateGitPass(str: string) {
|
|||
return gitPass ? str.replace(gitPass, 'GIT_PASS') : str;
|
||||
}
|
||||
|
||||
const debugMode = !!process.env.DOCUSAURUS_DEPLOY_DEBUG;
|
||||
|
||||
// Log executed commands so that user can figure out mistakes on his own
|
||||
// for example: https://github.com/facebook/docusaurus/issues/3875
|
||||
function shellExecLog(cmd: string) {
|
||||
function exec(cmd: string, options?: {log?: boolean; failfast?: boolean}) {
|
||||
const log = options?.log ?? true;
|
||||
const failfast = options?.failfast ?? false;
|
||||
try {
|
||||
const result = shell.exec(cmd);
|
||||
logger.info`code=${obfuscateGitPass(cmd)} subdue=${`code: ${result.code}`}`;
|
||||
// TODO migrate to execa(file,[...args]) instead
|
||||
// Use async/await everything
|
||||
// Avoid execa.command: the args need to be escaped manually
|
||||
const result = execa.commandSync(cmd);
|
||||
if (log || debugMode) {
|
||||
logger.info`code=${obfuscateGitPass(
|
||||
cmd,
|
||||
)} subdue=${`code: ${result.exitCode}`}`;
|
||||
}
|
||||
if (debugMode) {
|
||||
console.log(result);
|
||||
}
|
||||
if (failfast && result.exitCode !== 0) {
|
||||
throw new Error(
|
||||
`Command returned unexpected exitCode ${result.exitCode}`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
logger.error`code=${obfuscateGitPass(cmd)}`;
|
||||
throw err;
|
||||
throw new Error(
|
||||
logger.interpolate`Error while executing command code=${obfuscateGitPass(
|
||||
cmd,
|
||||
)}
|
||||
In CWD code=${process.cwd()}`,
|
||||
{cause: err},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Execa escape args and add necessary quotes automatically
|
||||
// When using Execa.command, the args containing spaces must be escaped manually
|
||||
function escapeArg(arg: string): string {
|
||||
return arg.replaceAll(' ', '\\ ');
|
||||
}
|
||||
|
||||
function hasGit() {
|
||||
return exec('git --version').exitCode === 0;
|
||||
}
|
||||
|
||||
export async function deploy(
|
||||
siteDirParam: string = '.',
|
||||
cliOptions: Partial<DeployCLIOptions> = {},
|
||||
|
@ -59,19 +93,26 @@ This behavior can have SEO impacts and create relative link issues.
|
|||
}
|
||||
|
||||
logger.info('Deploy command invoked...');
|
||||
if (!shell.which('git')) {
|
||||
throw new Error('Git not installed or on the PATH!');
|
||||
if (!hasGit()) {
|
||||
throw new Error('Git not installed or not added to 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();
|
||||
const {stdout} = exec('git remote get-url origin', {
|
||||
log: false,
|
||||
failfast: true,
|
||||
});
|
||||
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();
|
||||
exec('git rev-parse --abbrev-ref HEAD', {
|
||||
log: false,
|
||||
failfast: true,
|
||||
})
|
||||
?.stdout?.toString()
|
||||
.trim();
|
||||
|
||||
const gitUser = process.env.GIT_USER;
|
||||
|
||||
|
@ -116,8 +157,11 @@ 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);
|
||||
exec('echo "Skipping deploy on a pull request."', {
|
||||
log: false,
|
||||
failfast: true,
|
||||
});
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// github.io indicates organization repos that deploy via default branch. All
|
||||
|
@ -181,7 +225,7 @@ 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 = exec('git rev-parse HEAD')?.stdout?.toString().trim();
|
||||
|
||||
const runDeploy = async (outputDirectory: string) => {
|
||||
const targetDirectory = cliOptions.targetDir ?? '.';
|
||||
|
@ -189,22 +233,27 @@ 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);
|
||||
process.chdir(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
|
||||
// repository default branch.
|
||||
if (
|
||||
shellExecLog(
|
||||
`git clone --depth 1 --branch ${deploymentBranch} ${deploymentRepoURL} "${toPath}"`,
|
||||
).code !== 0
|
||||
exec(
|
||||
`git clone --depth 1 --branch ${deploymentBranch} ${deploymentRepoURL} ${escapeArg(
|
||||
toPath,
|
||||
)}`,
|
||||
).exitCode !== 0
|
||||
) {
|
||||
shellExecLog(`git clone --depth 1 ${deploymentRepoURL} "${toPath}"`);
|
||||
shellExecLog(`git checkout -b ${deploymentBranch}`);
|
||||
exec(`git clone --depth 1 ${deploymentRepoURL} ${escapeArg(toPath)}`);
|
||||
exec(`git checkout -b ${deploymentBranch}`);
|
||||
}
|
||||
|
||||
// Clear out any existing contents in the target directory
|
||||
shellExecLog(`git rm -rf ${targetDirectory}`);
|
||||
exec(`git rm -rf ${escapeArg(targetDirectory)}`, {
|
||||
log: false,
|
||||
failfast: true,
|
||||
});
|
||||
|
||||
const targetPath = path.join(toPath, targetDirectory);
|
||||
try {
|
||||
|
@ -213,29 +262,31 @@ You can also set the deploymentBranch property in docusaurus.config.js .`);
|
|||
logger.error`Copying build assets from path=${fromPath} to path=${targetPath} failed.`;
|
||||
throw err;
|
||||
}
|
||||
shellExecLog('git add --all');
|
||||
exec('git add --all', {failfast: true});
|
||||
|
||||
const gitUserName = process.env.GIT_USER_NAME;
|
||||
if (gitUserName) {
|
||||
shellExecLog(`git config user.name "${gitUserName}"`);
|
||||
exec(`git config user.name ${escapeArg(gitUserName)}`, {failfast: true});
|
||||
}
|
||||
|
||||
const gitUserEmail = process.env.GIT_USER_EMAIL;
|
||||
if (gitUserEmail) {
|
||||
shellExecLog(`git config user.email "${gitUserEmail}"`);
|
||||
exec(`git config user.email ${escapeArg(gitUserEmail)}`, {
|
||||
failfast: true,
|
||||
});
|
||||
}
|
||||
|
||||
const commitMessage =
|
||||
process.env.CUSTOM_COMMIT_MESSAGE ??
|
||||
`Deploy website - based on ${currentCommit}`;
|
||||
const commitResults = shellExecLog(`git commit -m "${commitMessage}"`);
|
||||
if (
|
||||
shellExecLog(`git push --force origin ${deploymentBranch}`).code !== 0
|
||||
) {
|
||||
const commitResults = exec(
|
||||
`git commit -m ${escapeArg(commitMessage)} --allow-empty`,
|
||||
);
|
||||
if (exec(`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') {
|
||||
|
@ -246,8 +297,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 {
|
||||
exec(`echo "Website is live at ${websiteURL}."`, {failfast: true});
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to execute command: ${err}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -8409,7 +8409,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==
|
||||
|
|
Loading…
Add table
Reference in a new issue