[cli] Add node_modules/.bin to PATH instead of running npx/yarn run (#8890)

`runDevCommand()` assumes the dev command is an npm package and thus uses `npx` or `yarn run` to execute it. In the case of a Hugo-based app, there is no npm package, so we want spawn to find Hugo in the `PATH`. Then for Node-based apps, instead of `npx`, spawn should find the command since `node_modules/.bin` has been added to the `PATH`.

### Related Issues

> https://github.com/vercel/customer-issues/issues/871

Note: This PR is a recreation of https://github.com/vercel/vercel/pull/8864 because prettier changed a bunch of Hugo files, which was bloating the original PR.

### 📋 Checklist

<!--
  Please keep your PR as a Draft until the checklist is complete
-->

#### Tests

- [x] The code changed/added as part of this PR has been covered with tests
- [x] All tests pass locally with `yarn test-unit`

#### Code Review

- [ ] This PR has a concise title and thorough description useful to a reviewer
- [ ] Issue from task tracker has a link to this PR
This commit is contained in:
Chris Barber
2022-11-14 09:26:27 -06:00
committed by GitHub
parent b572ef5d71
commit f283b3b106
5 changed files with 68 additions and 31 deletions

View File

@@ -172,7 +172,6 @@
"typescript": "4.7.4", "typescript": "4.7.4",
"universal-analytics": "0.4.20", "universal-analytics": "0.4.20",
"utility-types": "2.1.0", "utility-types": "2.1.0",
"which": "2.0.2",
"write-json-file": "2.2.0", "write-json-file": "2.2.0",
"xdg-app-paths": "5.1.0", "xdg-app-paths": "5.1.0",
"yauzl-promise": "2.1.3" "yauzl-promise": "2.1.3"

View File

@@ -18,7 +18,6 @@ import directoryTemplate from 'serve-handler/src/directory';
import getPort from 'get-port'; import getPort from 'get-port';
import isPortReachable from 'is-port-reachable'; import isPortReachable from 'is-port-reachable';
import deepEqual from 'fast-deep-equal'; import deepEqual from 'fast-deep-equal';
import which from 'which';
import npa from 'npm-package-arg'; import npa from 'npm-package-arg';
import type { ChildProcess } from 'child_process'; import type { ChildProcess } from 'child_process';
@@ -33,6 +32,7 @@ import {
Builder, Builder,
cloneEnv, cloneEnv,
Env, Env,
getNodeBinPath,
StartDevServerResult, StartDevServerResult,
FileFsRef, FileFsRef,
PackageJson, PackageJson,
@@ -2238,6 +2238,10 @@ export default class DevServer {
} }
); );
// add the node_modules/.bin directory to the PATH
const nodeBinPath = await getNodeBinPath({ cwd });
env.PATH = `${nodeBinPath}${path.delimiter}${env.PATH}`;
// This is necesary so that the dev command in the Project // This is necesary so that the dev command in the Project
// will work cross-platform (especially Windows). // will work cross-platform (especially Windows).
let command = devCommand let command = devCommand
@@ -2252,22 +2256,6 @@ export default class DevServer {
})}` })}`
); );
const isNpxAvailable = await which('npx')
.then(() => true)
.catch(() => false);
if (isNpxAvailable) {
command = `npx --no-install ${command}`;
} else {
const isYarnAvailable = await which('yarn')
.then(() => true)
.catch(() => false);
if (isYarnAvailable) {
command = `yarn run --silent ${command}`;
}
}
this.output.debug(`Spawning dev command: ${command}`); this.output.debug(`Spawning dev command: ${command}`);
const proxyPort = new RegExp(port.toString(), 'g'); const proxyPort = new RegExp(port.toString(), 'g');

View File

@@ -104,13 +104,38 @@ test(
test('[vercel dev] 08-hugo', async () => { test('[vercel dev] 08-hugo', async () => {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
// Update PATH to find the Hugo executable installed via GH Actions // 1. run the test without Hugo in the PATH
let tester = await testFixtureStdio(
'08-hugo',
async () => {
throw new Error('Expected dev server to fail to be ready');
},
{
readyTimeout: 2000,
// Important: for the first test, we MUST deploy this app so that the
// framework (e.g. Hugo) will be detected by the server and associated
// with the project since `vc dev` doesn't do framework detection
skipDeploy: false,
}
);
await expect(tester()).rejects.toThrow(
new Error('Dev server timed out while waiting to be ready')
);
// 2. Update PATH to find the Hugo executable installed via GH Actions
process.env.PATH = `${resolve(fixture('08-hugo'))}${delimiter}${ process.env.PATH = `${resolve(fixture('08-hugo'))}${delimiter}${
process.env.PATH process.env.PATH
}`; }`;
const tester = testFixtureStdio('08-hugo', async (testPath: any) => {
// 3. Rerun the test now that Hugo is in the PATH
tester = testFixtureStdio(
'08-hugo',
async (testPath: any) => {
await testPath(200, '/', /Hugo/m); await testPath(200, '/', /Hugo/m);
}); },
{ skipDeploy: true }
);
await tester(); await tester();
} else { } else {
console.log(`Skipping 08-hugo on platform ${process.platform}`); console.log(`Skipping 08-hugo on platform ${process.platform}`);

View File

@@ -61,8 +61,13 @@ function fetchWithRetry(url, opts = {}) {
function createResolver() { function createResolver() {
let resolver; let resolver;
const p = new Promise(res => (resolver = res)); let rejector;
const p = new Promise((resolve, reject) => {
resolver = resolve;
rejector = reject;
});
p.resolve = resolver; p.resolve = resolver;
p.reject = rejector;
return p; return p;
} }
@@ -274,7 +279,13 @@ async function testFixture(directory, opts = {}, args = []) {
function testFixtureStdio( function testFixtureStdio(
directory, directory,
fn, fn,
{ expectedCode = 0, skipDeploy, isExample, projectSettings } = {} {
expectedCode = 0,
skipDeploy,
isExample,
projectSettings,
readyTimeout = 0,
} = {}
) { ) {
return async () => { return async () => {
const nodeMajor = Number(process.versions.node.split('.')[0]); const nodeMajor = Number(process.versions.node.split('.')[0]);
@@ -385,6 +396,18 @@ function testFixtureStdio(
const readyResolver = createResolver(); const readyResolver = createResolver();
const exitResolver = createResolver(); const exitResolver = createResolver();
// By default, tests will wait 6 minutes for the dev server to be ready and
// perform the tests, however a `readyTimeout` can be used to reduce the
// wait time if the dev server is expected to fail to start or hang
let readyTimer = null;
if (readyTimeout > 0) {
readyTimer = setTimeout(() => {
readyResolver.reject(
new Error('Dev server timed out while waiting to be ready')
);
}, readyTimeout);
}
try { try {
let printedOutput = false; let printedOutput = false;
@@ -424,6 +447,7 @@ function testFixtureStdio(
stderr += data; stderr += data;
if (stripAnsi(data).includes('Ready! Available at')) { if (stripAnsi(data).includes('Ready! Available at')) {
clearTimeout(readyTimer);
readyResolver.resolve(); readyResolver.resolve();
} }
@@ -507,5 +531,6 @@ module.exports = {
shouldSkip, shouldSkip,
fixture, fixture,
fetch, fetch,
fetchWithRetry,
validateResponseHeaders, validateResponseHeaders,
}; };

View File

@@ -13677,13 +13677,6 @@ which-module@^2.0.0:
resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a"
integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=
which@2.0.2, which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
which@^1.2.9, which@^1.3.1: which@^1.2.9, which@^1.3.1:
version "1.3.1" version "1.3.1"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a"
@@ -13691,6 +13684,13 @@ which@^1.2.9, which@^1.3.1:
dependencies: dependencies:
isexe "^2.0.0" isexe "^2.0.0"
which@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
dependencies:
isexe "^2.0.0"
wide-align@^1.1.0: wide-align@^1.1.0:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"