diff --git a/src/utils/shell.ts b/src/utils/shell.ts index 62f8adbf95e..898bd3753eb 100644 --- a/src/utils/shell.ts +++ b/src/utils/shell.ts @@ -68,6 +68,9 @@ export const runCommand = ( const { cwd, env = {}, spinner } = options const commandProcess = execa.command(command, { preferLocal: true, + // Command strings in netlify.toml may use shell operators like `&&`. + // execa.command() does not interpret these unless shell mode is enabled. + shell: true, // we use reject=false to avoid rejecting synchronously when the command doesn't exist reject: false, env: { diff --git a/tests/integration/framework-detection.test.ts b/tests/integration/framework-detection.test.ts index fe32530e507..fa1e75338ce 100644 --- a/tests/integration/framework-detection.test.ts +++ b/tests/integration/framework-detection.test.ts @@ -130,6 +130,25 @@ describe.concurrent('frameworks/framework-detection', () => { }) }) + test('should support shell operators in `command`', async (t) => { + await withSiteBuilder(t, async (builder) => { + await builder + .withNetlifyToml({ config: { dev: { command: 'echo first && echo second', targetPort: 3000 } } }) + .build() + + try { + await withDevServer({ cwd: builder.directory }, async () => {}, true) + // a failure is expected since this command does not start a server + t.expect.unreachable() + } catch (err) { + t.expect(err).toHaveProperty('stdout') + const output = normalizeSnapshot((err as execa.ExecaReturnValue).stdout, { duration: true, filePath: true }) + t.expect(output).toMatch(/\nfirst\s*\r?\n\s*second\r?\n/i) + t.expect(output).not.toMatch(/\nfirst\s*&&\s*echo\s+second\r?\n/i) + } + }) + }) + test('should force a specific framework when configured', async (t) => { await withSiteBuilder(t, async (builder) => { await builder.withNetlifyToml({ config: { dev: { framework: 'create-react-app' } } }).build()