|
1 | | -import {cp, mkdir} from 'node:fs/promises'; |
| 1 | +import {cp, mkdir, unlink} from 'node:fs/promises'; |
2 | 2 | import path from 'node:path'; |
3 | 3 | import process from 'node:process'; |
4 | 4 | import test from 'ava'; |
@@ -166,30 +166,114 @@ test('Does not mutate arguments nor options', testNoMutation, {}); |
166 | 166 | test('Does not mutate arguments nor options with a shell', testNoMutation, {shell: true}); |
167 | 167 |
|
168 | 168 | if (isWindows) { |
| 169 | + const nodeOnlyOptions = { |
| 170 | + extendEnv: false, |
| 171 | + env: { |
| 172 | + Path: path.dirname(process.execPath), |
| 173 | + PathExt: '.EXE', |
| 174 | + }, |
| 175 | + }; |
| 176 | + |
169 | 177 | // A bare command name without an extension is resolved using `PATHEXT`. |
170 | 178 | test('Resolves command extension using PATHEXT (sync)', t => { |
171 | 179 | const {stdout} = execaSync('hello'); |
172 | 180 | t.is(stdout, 'Hello World'); |
173 | 181 | }); |
174 | 182 |
|
175 | | - test('Runs a .com executable selected by PATHEXT', async t => { |
| 183 | + test('Uses PATHEXT extension order for direct executables', async t => { |
176 | 184 | const binaryDirectory = path.join(FIXTURES_DIRECTORY, 'node_modules', '.bin'); |
177 | 185 | await mkdir(binaryDirectory, {recursive: true}); |
178 | | - await cp(process.execPath, path.join(binaryDirectory, 'node-com.com')); |
| 186 | + const command = 'node-extension-order'; |
| 187 | + const executablePath = path.join(binaryDirectory, `${command}.exe`); |
| 188 | + const comPath = path.join(binaryDirectory, `${command}.com`); |
| 189 | + await Promise.all([ |
| 190 | + cp(process.execPath, executablePath), |
| 191 | + cp(process.execPath, comPath), |
| 192 | + ]); |
| 193 | + t.teardown(async () => { |
| 194 | + await Promise.all([unlink(executablePath), unlink(comPath)]); |
| 195 | + }); |
179 | 196 | const options = { |
180 | 197 | preferLocal: true, |
181 | 198 | localDir: FIXTURES_DIRECTORY, |
182 | 199 | extendEnv: false, |
183 | 200 | env: { |
184 | 201 | Path: path.dirname(process.execPath), |
185 | | - PathExt: '.COM', |
| 202 | + PathExt: '.EXE;.COM', |
| 203 | + }, |
| 204 | + }; |
| 205 | + const nodeExpression = 'JSON.stringify({executablePath: process.execPath, argv0: process.argv0})'; |
| 206 | + const {stdout} = await execa(command, ['--print', nodeExpression], options); |
| 207 | + const {executablePath: actualExecutablePath, argv0} = JSON.parse(stdout); |
| 208 | + t.is(actualExecutablePath.toLowerCase(), executablePath.toLowerCase()); |
| 209 | + t.is(argv0, command); |
| 210 | + |
| 211 | + const {stdout: stdoutSync} = execaSync(command, ['--print', nodeExpression], options); |
| 212 | + const {executablePath: actualExecutablePathSync, argv0: argv0Sync} = JSON.parse(stdoutSync); |
| 213 | + t.is(actualExecutablePathSync.toLowerCase(), executablePath.toLowerCase()); |
| 214 | + t.is(argv0Sync, command); |
| 215 | + }); |
| 216 | + |
| 217 | + test.serial('Respects NoDefaultCurrentDirectoryInExePath', async t => { |
| 218 | + const binaryDirectory = path.join(FIXTURES_DIRECTORY, 'node_modules', '.bin'); |
| 219 | + await mkdir(binaryDirectory, {recursive: true}); |
| 220 | + const command = 'node-current-directory'; |
| 221 | + const currentDirectoryExecutable = path.join(FIXTURES_DIRECTORY, `${command}.exe`); |
| 222 | + const pathExecutable = path.join(binaryDirectory, `${command}.exe`); |
| 223 | + await Promise.all([ |
| 224 | + cp(process.execPath, currentDirectoryExecutable), |
| 225 | + cp(process.execPath, pathExecutable), |
| 226 | + ]); |
| 227 | + |
| 228 | + const environmentName = 'NoDefaultCurrentDirectoryInExePath'; |
| 229 | + const originalValue = process.env[environmentName]; |
| 230 | + process.env[environmentName] = '1'; |
| 231 | + t.teardown(async () => { |
| 232 | + if (originalValue === undefined) { |
| 233 | + delete process.env[environmentName]; |
| 234 | + } else { |
| 235 | + process.env[environmentName] = originalValue; |
| 236 | + } |
| 237 | + |
| 238 | + await Promise.all([unlink(currentDirectoryExecutable), unlink(pathExecutable)]); |
| 239 | + }); |
| 240 | + |
| 241 | + const options = { |
| 242 | + cwd: FIXTURES_DIRECTORY, |
| 243 | + extendEnv: false, |
| 244 | + env: { |
| 245 | + Path: binaryDirectory, |
| 246 | + PathExt: '.EXE', |
186 | 247 | }, |
187 | 248 | }; |
188 | | - const {stdout} = await execa('node-com', ['--version'], options); |
189 | | - t.is(stdout, process.version); |
| 249 | + const {stdout} = await execa(command, ['--print', 'process.execPath'], options); |
| 250 | + t.is(stdout.toLowerCase(), pathExecutable.toLowerCase()); |
| 251 | + |
| 252 | + const {stdout: stdoutSync} = execaSync(command, ['--print', 'process.execPath'], options); |
| 253 | + t.is(stdoutSync.toLowerCase(), pathExecutable.toLowerCase()); |
| 254 | + }); |
190 | 255 |
|
191 | | - const {stdout: stdoutSync} = execaSync('node-com', ['--version'], options); |
192 | | - t.is(stdoutSync, process.version); |
| 256 | + test('Does not search PATH for drive-relative commands', async t => { |
| 257 | + const binaryDirectory = path.join(FIXTURES_DIRECTORY, 'node_modules', '.bin'); |
| 258 | + await mkdir(binaryDirectory, {recursive: true}); |
| 259 | + const commandName = 'node-drive-relative'; |
| 260 | + const pathExecutable = path.join(binaryDirectory, `${commandName}.exe`); |
| 261 | + await cp(process.execPath, pathExecutable); |
| 262 | + t.teardown(async () => { |
| 263 | + await unlink(pathExecutable); |
| 264 | + }); |
| 265 | + const drive = path.parse(FIXTURES_DIRECTORY).root.slice(0, 2); |
| 266 | + const command = `${drive}${commandName}`; |
| 267 | + const options = { |
| 268 | + cwd: FIXTURES_DIRECTORY, |
| 269 | + extendEnv: false, |
| 270 | + env: { |
| 271 | + Path: binaryDirectory, |
| 272 | + PathExt: '.EXE', |
| 273 | + }, |
| 274 | + }; |
| 275 | + await t.throwsAsync(execa(command, [], options)); |
| 276 | + t.throws(() => execaSync(command, [], options)); |
193 | 277 | }); |
194 | 278 |
|
195 | 279 | // A `.cmd` file needs `cmd.exe`, so its forward-slash path must be normalized to |
@@ -223,31 +307,17 @@ if (isWindows) { |
223 | 307 |
|
224 | 308 | test('Double-escapes explicit batch files excluded from PATHEXT', async t => { |
225 | 309 | const commandArgument = '"& whoami &"'; |
226 | | - const options = { |
227 | | - extendEnv: false, |
228 | | - env: { |
229 | | - Path: path.dirname(process.execPath), |
230 | | - PathExt: '.EXE', |
231 | | - }, |
232 | | - }; |
233 | 310 | const command = path.join(FIXTURES_DIRECTORY, 'echo-shim.cmd'); |
234 | | - const {stdout} = await execa(command, [commandArgument], options); |
| 311 | + const {stdout} = await execa(command, [commandArgument], nodeOnlyOptions); |
235 | 312 | t.is(stdout, commandArgument); |
236 | 313 |
|
237 | | - const {stdout: stdoutSync} = execaSync(command, [commandArgument], options); |
| 314 | + const {stdout: stdoutSync} = execaSync(command, [commandArgument], nodeOnlyOptions); |
238 | 315 | t.is(stdoutSync, commandArgument); |
239 | 316 | }); |
240 | 317 |
|
241 | 318 | test('Runs an explicit shebang script excluded from PATHEXT', async t => { |
242 | 319 | const command = path.join(FIXTURES_DIRECTORY, 'echo.js'); |
243 | | - const options = { |
244 | | - extendEnv: false, |
245 | | - env: { |
246 | | - Path: path.dirname(process.execPath), |
247 | | - PathExt: '.EXE', |
248 | | - }, |
249 | | - }; |
250 | | - await testResolvesCommand(t, command, options); |
| 320 | + await testResolvesCommand(t, command, nodeOnlyOptions); |
251 | 321 | }); |
252 | 322 |
|
253 | 323 | test.serial('Double-escapes metacharacters for preferLocal cmd-shims', async t => { |
|
0 commit comments