diff --git a/src/commands/base-command.ts b/src/commands/base-command.ts index 707bd201b64..1f95e0b54e1 100644 --- a/src/commands/base-command.ts +++ b/src/commands/base-command.ts @@ -1,3 +1,5 @@ +import { isCI } from 'ci-info' + import { existsSync } from 'fs' import { join, relative, resolve } from 'path' import process from 'process' @@ -103,6 +105,16 @@ async function selectWorkspace(project: Project, filter?: string): Promise pkg.name || pkg.path) + .join( + ', ', + )}. Configure the site you want to work with and try again. Refer to https://ntl.fyi/configure-site for more information.`, + ) + } + const { result } = await inquirer.prompt({ name: 'result', // @ts-expect-error TS(2769) FIXME: No overload matches this call. diff --git a/src/utils/build-info.ts b/src/utils/build-info.ts index 9250493949b..5bc3a5caf6b 100644 --- a/src/utils/build-info.ts +++ b/src/utils/build-info.ts @@ -1,4 +1,5 @@ import { Settings } from '@netlify/build-info' +import { isCI } from 'ci-info' import fuzzy from 'fuzzy' import inquirer from 'inquirer' @@ -77,6 +78,17 @@ export const detectFrameworkSettings = async ( } if (settings.length > 1) { + if (isCI) { + log(`Multiple possible ${type} commands found`) + throw new Error( + `Detected commands for: ${settings + .map((setting) => setting.framework.name) + .join( + ', ', + )}. Update your settings to specify which to use. Refer to https://ntl.fyi/dev-monorepo for more information.`, + ) + } + // multiple matching detectors, make the user choose const scriptInquirerOptions = formatSettingsArrForInquirer(settings, type) const { chosenSettings } = await inquirer.prompt<{ chosenSettings: Settings }>({ diff --git a/tests/integration/commands/dev/dev-miscellaneous.test.js b/tests/integration/commands/dev/dev-miscellaneous.test.js index e25ebc1a1b1..ba6f03dbb07 100644 --- a/tests/integration/commands/dev/dev-miscellaneous.test.js +++ b/tests/integration/commands/dev/dev-miscellaneous.test.js @@ -3,16 +3,19 @@ import path from 'path' import { fileURLToPath } from 'url' import { setProperty } from 'dot-prop' +import execa from 'execa' import getAvailablePort from 'get-port' import jwt from 'jsonwebtoken' import fetch from 'node-fetch' import { describe, test } from 'vitest' -import { withDevServer } from '../../utils/dev-server.ts' +import { cliPath } from '../../utils/cli-path.js' +import { getExecaOptions, withDevServer } from '../../utils/dev-server.ts' import got from '../../utils/got.js' import { withMockApi } from '../../utils/mock-api.js' import { pause } from '../../utils/pause.js' import { withSiteBuilder } from '../../utils/site-builder.ts' +import { normalize } from '../../utils/snapshots.js' // eslint-disable-next-line no-underscore-dangle const __dirname = path.dirname(fileURLToPath(import.meta.url)) @@ -1318,4 +1321,30 @@ describe.concurrent('commands/dev-miscellaneous', () => { ) }) }) + + test('should fail in CI with multiple projects', async (t) => { + await withSiteBuilder('site-with-multiple-packages', async (builder) => { + await builder + .withPackageJson({ packageJson: { name: 'main', workspaces: ['*'] } }) + .withPackageJson({ packageJson: { name: 'package1' }, pathPrefix: 'package1' }) + .withPackageJson({ packageJson: { name: 'package2' }, pathPrefix: 'package2' }) + .buildAsync() + + const asyncErrorBlock = async () => { + const childProcess = execa( + cliPath, + ['dev', '--offline'], + getExecaOptions({ cwd: builder.directory, env: { CI: true } }), + ) + await childProcess + } + const error = await asyncErrorBlock().catch((error_) => error_) + t.expect( + normalize(error.stderr, { duration: true, filePath: true }).includes( + 'Sites detected: package1, package2. Configure the site you want to work with and try again. Refer to https://ntl.fyi/configure-site for more information.', + ), + ) + t.expect(error.exitCode).toBe(1) + }) + }) }) diff --git a/tests/integration/framework-detection.test.js b/tests/integration/framework-detection.test.js index cbdcbc61e35..1ab1ce5a12b 100644 --- a/tests/integration/framework-detection.test.js +++ b/tests/integration/framework-detection.test.js @@ -236,7 +236,11 @@ describe.concurrent('frameworks/framework-detection', () => { // a failure is expected since this is not a true framework project const asyncErrorBlock = async () => { - const childProcess = execa(cliPath, ['dev', '--offline'], getExecaOptions({ cwd: builder.directory })) + const childProcess = execa( + cliPath, + ['dev', '--offline'], + getExecaOptions({ cwd: builder.directory, env: { CI: 'false' } }), + ) handleQuestions(childProcess, [ { @@ -252,6 +256,37 @@ describe.concurrent('frameworks/framework-detection', () => { }) }) + test('should fail in CI when multiple frameworks are detected', async (t) => { + await withSiteBuilder('site-with-multiple-frameworks', async (builder) => { + await builder + .withPackageJson({ + packageJson: { + dependencies: { 'react-scripts': '1.0.0', gatsby: '^3.0.0' }, + scripts: { start: 'react-scripts start', develop: 'gatsby develop' }, + }, + }) + .withContentFile({ path: 'gatsby-config.js', content: '' }) + .buildAsync() + + // a failure is expected since this is not a true framework project + const asyncErrorBlock = async () => { + const childProcess = execa( + cliPath, + ['dev', '--offline'], + getExecaOptions({ cwd: builder.directory, env: { CI: true } }), + ) + await childProcess + } + const error = await asyncErrorBlock().catch((error_) => error_) + t.expect( + normalize(error.stdout, { duration: true, filePath: true }).includes( + 'Detected commands for: Gatsby, Create React App. Update your settings to specify which to use. Refer to https://ntl.fyi/dev-monorepo for more information.', + ), + ) + t.expect(error.exitCode).toBe(1) + }) + }) + test('should not run framework detection if command and targetPort are configured', async (t) => { await withSiteBuilder('site-with-hugo-config', async (builder) => { await builder.withContentFile({ path: 'config.toml', content: '' }).buildAsync()