-
-
Notifications
You must be signed in to change notification settings - Fork 217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve cwd
option
#803
Improve cwd
option
#803
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import {statSync} from 'node:fs'; | ||
import {resolve} from 'node:path'; | ||
import {fileURLToPath} from 'node:url'; | ||
import process from 'node:process'; | ||
|
||
export const getDefaultCwd = () => { | ||
try { | ||
return process.cwd(); | ||
} catch (error) { | ||
error.message = `The current directory does not exist.\n${error.message}`; | ||
throw error; | ||
} | ||
}; | ||
|
||
export const normalizeCwd = cwd => { | ||
const cwdString = safeNormalizeFileUrl(cwd, 'The "cwd" option'); | ||
return resolve(cwdString); | ||
}; | ||
|
||
export const safeNormalizeFileUrl = (file, name) => { | ||
const fileString = normalizeFileUrl(file); | ||
|
||
if (typeof fileString !== 'string') { | ||
throw new TypeError(`${name} must be a string or a file URL: ${fileString}.`); | ||
} | ||
|
||
return fileString; | ||
}; | ||
|
||
export const normalizeFileUrl = file => file instanceof URL ? fileURLToPath(file) : file; | ||
|
||
export const fixCwdError = (originalMessage, cwd) => { | ||
if (cwd === getDefaultCwd()) { | ||
return originalMessage; | ||
} | ||
|
||
let cwdStat; | ||
try { | ||
cwdStat = statSync(cwd); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is used both by
So I thought it might be ok to keep it simple for the time being. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
👍 |
||
} catch (error) { | ||
return `The "cwd" option is invalid: ${cwd}.\n${error.message}\n${originalMessage}`; | ||
} | ||
|
||
if (!cwdStat.isDirectory()) { | ||
return `The "cwd" option is not a directory: ${cwd}.\n${originalMessage}`; | ||
} | ||
|
||
return originalMessage; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import {mkdir, rmdir} from 'node:fs/promises'; | ||
import {relative, toNamespacedPath} from 'node:path'; | ||
import process from 'node:process'; | ||
import {pathToFileURL, fileURLToPath} from 'node:url'; | ||
import tempfile from 'tempfile'; | ||
import test from 'ava'; | ||
import {execa, execaSync} from '../index.js'; | ||
import {FIXTURES_DIR, setFixtureDir} from './helpers/fixtures-dir.js'; | ||
|
||
setFixtureDir(); | ||
|
||
const isWindows = process.platform === 'win32'; | ||
|
||
const testOptionCwdString = async (t, execaMethod) => { | ||
const cwd = '/'; | ||
const {stdout} = await execaMethod('node', ['-p', 'process.cwd()'], {cwd}); | ||
t.is(toNamespacedPath(stdout), toNamespacedPath(cwd)); | ||
}; | ||
|
||
test('The "cwd" option can be a string', testOptionCwdString, execa); | ||
test('The "cwd" option can be a string - sync', testOptionCwdString, execaSync); | ||
|
||
const testOptionCwdUrl = async (t, execaMethod) => { | ||
const cwd = '/'; | ||
const cwdUrl = pathToFileURL(cwd); | ||
const {stdout} = await execaMethod('node', ['-p', 'process.cwd()'], {cwd: cwdUrl}); | ||
t.is(toNamespacedPath(stdout), toNamespacedPath(cwd)); | ||
}; | ||
|
||
test('The "cwd" option can be a URL', testOptionCwdUrl, execa); | ||
test('The "cwd" option can be a URL - sync', testOptionCwdUrl, execaSync); | ||
|
||
const testOptionCwdInvalid = (t, execaMethod) => { | ||
t.throws(() => { | ||
execaMethod('empty.js', {cwd: true}); | ||
}, {message: /The "cwd" option must be a string or a file URL: true/}); | ||
}; | ||
|
||
test('The "cwd" option cannot be an invalid type', testOptionCwdInvalid, execa); | ||
test('The "cwd" option cannot be an invalid type - sync', testOptionCwdInvalid, execaSync); | ||
|
||
const testErrorCwdDefault = async (t, execaMethod) => { | ||
const {cwd} = await execaMethod('empty.js'); | ||
t.is(cwd, process.cwd()); | ||
}; | ||
|
||
test('The "cwd" option defaults to process.cwd()', testErrorCwdDefault, execa); | ||
test('The "cwd" option defaults to process.cwd() - sync', testErrorCwdDefault, execaSync); | ||
|
||
// Windows does not allow removing a directory used as `cwd` of a running process | ||
if (!isWindows) { | ||
const testCwdPreSpawn = async (t, execaMethod) => { | ||
const currentCwd = process.cwd(); | ||
const filePath = tempfile(); | ||
await mkdir(filePath); | ||
process.chdir(filePath); | ||
await rmdir(filePath); | ||
|
||
try { | ||
t.throws(() => { | ||
execaMethod('empty.js'); | ||
}, {message: /The current directory does not exist/}); | ||
} finally { | ||
process.chdir(currentCwd); | ||
} | ||
}; | ||
|
||
test.serial('The "cwd" option default fails if current cwd is missing', testCwdPreSpawn, execa); | ||
test.serial('The "cwd" option default fails if current cwd is missing - sync', testCwdPreSpawn, execaSync); | ||
} | ||
|
||
const cwdNotExisting = {cwd: 'does_not_exist', expectedCode: 'ENOENT', expectedMessage: 'The "cwd" option is invalid'}; | ||
const cwdTooLong = {cwd: '.'.repeat(1e5), expectedCode: 'ENAMETOOLONG', expectedMessage: 'The "cwd" option is invalid'}; | ||
const cwdNotDir = {cwd: fileURLToPath(import.meta.url), expectedCode: isWindows ? 'ENOENT' : 'ENOTDIR', expectedMessage: 'The "cwd" option is not a directory'}; | ||
|
||
const testCwdPostSpawn = async (t, {cwd, expectedCode, expectedMessage}, execaMethod) => { | ||
const {failed, code, message} = await execaMethod('empty.js', {cwd, reject: false}); | ||
t.true(failed); | ||
t.is(code, expectedCode); | ||
t.true(message.includes(expectedMessage)); | ||
t.true(message.includes(cwd)); | ||
}; | ||
|
||
test('The "cwd" option must be an existing file', testCwdPostSpawn, cwdNotExisting, execa); | ||
test('The "cwd" option must be an existing file - sync', testCwdPostSpawn, cwdNotExisting, execaSync); | ||
test('The "cwd" option must not be too long', testCwdPostSpawn, cwdTooLong, execa); | ||
test('The "cwd" option must not be too long - sync', testCwdPostSpawn, cwdTooLong, execaSync); | ||
test('The "cwd" option must be a directory', testCwdPostSpawn, cwdNotDir, execa); | ||
test('The "cwd" option must be a directory - sync', testCwdPostSpawn, cwdNotDir, execaSync); | ||
|
||
const successProperties = {fixtureName: 'empty.js', expectedFailed: false}; | ||
const errorProperties = {fixtureName: 'fail.js', expectedFailed: true}; | ||
|
||
const testErrorCwd = async (t, execaMethod, {fixtureName, expectedFailed}) => { | ||
const {failed, cwd} = await execaMethod(fixtureName, {cwd: relative('.', FIXTURES_DIR), reject: false}); | ||
t.is(failed, expectedFailed); | ||
t.is(cwd, FIXTURES_DIR); | ||
}; | ||
|
||
test('result.cwd is defined', testErrorCwd, execa, successProperties); | ||
test('result.cwd is defined - sync', testErrorCwd, execaSync, successProperties); | ||
test('error.cwd is defined', testErrorCwd, execa, errorProperties); | ||
test('error.cwd is defined - sync', testErrorCwd, execaSync, errorProperties); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,15 @@ | ||
import path from 'node:path'; | ||
import {delimiter, resolve} from 'node:path'; | ||
import process from 'node:process'; | ||
import {fileURLToPath} from 'node:url'; | ||
import pathKey from 'path-key'; | ||
|
||
export const PATH_KEY = pathKey(); | ||
export const FIXTURES_DIR_URL = new URL('../fixtures/', import.meta.url); | ||
export const FIXTURES_DIR = fileURLToPath(FIXTURES_DIR_URL); | ||
export const FIXTURES_DIR = resolve(fileURLToPath(FIXTURES_DIR_URL)); | ||
|
||
// Add the fixtures directory to PATH so fixtures can be executed without adding | ||
// `node`. This is only meant to make writing tests simpler. | ||
export const setFixtureDir = () => { | ||
process.env[PATH_KEY] = FIXTURES_DIR + path.delimiter + process.env[PATH_KEY]; | ||
process.env[PATH_KEY] = FIXTURES_DIR + delimiter + process.env[PATH_KEY]; | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We cannot perform this
cwd
validation check before process spawning because it involves making astat
I/O call. Right now,execa()
spawns processes right away, and introducing a sync I/O call before spawning for everyexeca()
call is not worth it, just to improve thecwd
validation error message.That being said, since
process.cwd()
is synchronous, when thecwd
option default value is used, it is validated before process spawning.