diff --git a/.changeset/nervous-wolves-march.md b/.changeset/nervous-wolves-march.md new file mode 100644 index 000000000..6d4fd93e7 --- /dev/null +++ b/.changeset/nervous-wolves-march.md @@ -0,0 +1,5 @@ +--- +'@cloudflare/next-on-pages': patch +--- + +improve the error message shown when the Vercel build fails to make clearer that the issue is not next-on-pages related diff --git a/package-lock.json b/package-lock.json index abd9334e9..1b77f22c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cloudflare/next-on-pages", - "version": "0.9.0", + "version": "0.10.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@cloudflare/next-on-pages", - "version": "0.9.0", + "version": "0.10.0", "dependencies": { "acorn": "^8.8.0", "ast-types": "^0.14.2", diff --git a/src/buildApplication/buildApplication.ts b/src/buildApplication/buildApplication.ts index c0ff746dd..0e00e3e27 100644 --- a/src/buildApplication/buildApplication.ts +++ b/src/buildApplication/buildApplication.ts @@ -18,6 +18,7 @@ import { getVercelStaticAssets, processVercelOutput, } from './processVercelOutput'; +import { getCurrentPackageExecuter } from './packageManagerUtils'; /** * Builds the _worker.js with static assets implementing the Next.js application @@ -32,10 +33,13 @@ export async function buildApplication({ if (!skipBuild) { try { await buildVercelOutput(); - } catch (err) { + } catch { cliError( - (err as Error)?.message ?? - 'Error: The Vercel build failed. For more details see the Vercel logs above.' + ` + The Vercel build (\`${await getCurrentPackageExecuter()} vercel build\`) command failed. For more details see the Vercel logs above. + If you need help solving the issue, refer to the Vercel or Next.js documentation or their repositories. + `, + { spaced: true } ); buildSuccess = false; } diff --git a/src/buildApplication/buildVercelOutput.ts b/src/buildApplication/buildVercelOutput.ts index be4a7cb90..91e3accd9 100644 --- a/src/buildApplication/buildVercelOutput.ts +++ b/src/buildApplication/buildVercelOutput.ts @@ -3,9 +3,12 @@ import { spawn } from 'child_process'; import { join, resolve } from 'path'; import { cliLog } from '../cli'; import { validateDir, validateFile } from '../utils'; -import { getCurrentPackageManager } from './getCurrentPackageManager'; -import type { PackageManager } from '../utils/getSpawnCommand'; -import { getSpawnCommand } from '../utils/getSpawnCommand'; +import type { PackageManager } from './packageManagerUtils'; +import { + getCurrentPackageExecuter, + getCurrentPackageManager, + getPackageManagerSpawnCommand, +} from './packageManagerUtils'; /** * Builds the Next.js output via the Vercel CLI @@ -25,7 +28,7 @@ export async function buildVercelOutput(): Promise { await generateProjectJsonFileIfNeeded(); cliLog('Project is ready'); await runVercelBuild(pkgMng); - cliLog('Completed `npx vercel build`.\n'); + cliLog(`Completed \`${await getCurrentPackageExecuter()} vercel build\`.`); } /** @@ -44,7 +47,7 @@ async function generateProjectJsonFileIfNeeded(): Promise { } async function runVercelBuild(pkgMng: PackageManager): Promise { - const pkgMngCMD = getSpawnCommand(pkgMng); + const pkgMngCMD = getPackageManagerSpawnCommand(pkgMng); if (pkgMng === 'yarn (classic)') { cliLog( diff --git a/src/buildApplication/getCurrentPackageManager.ts b/src/buildApplication/packageManagerUtils.ts similarity index 60% rename from src/buildApplication/getCurrentPackageManager.ts rename to src/buildApplication/packageManagerUtils.ts index 376a8429b..64faffd78 100644 --- a/src/buildApplication/getCurrentPackageManager.ts +++ b/src/buildApplication/packageManagerUtils.ts @@ -1,9 +1,8 @@ import YAML from 'js-yaml'; import { spawn } from 'child_process'; import { readFile } from 'fs/promises'; -import type { PackageManager } from '../utils'; -import { validateFile, getSpawnCommand } from '../utils'; import { cliError } from '../cli'; +import { validateFile } from '../utils'; export async function getCurrentPackageManager(): Promise { const userAgent = process.env.npm_config_user_agent; @@ -14,7 +13,7 @@ export async function getCurrentPackageManager(): Promise { if ((userAgent && userAgent.startsWith('pnpm')) || hasPnpmLock) return 'pnpm'; if ((userAgent && userAgent.startsWith('yarn')) || hasYarnLock) { - const yarn = getSpawnCommand('yarn'); + const yarn = getPackageManagerSpawnCommand('yarn'); const getYarnV = spawn(yarn, ['-v']); let yarnV = ''; getYarnV.stdout.on('data', data => { @@ -49,3 +48,41 @@ export async function getCurrentPackageManager(): Promise { } return 'npm'; } + +export async function getCurrentPackageExecuter(): Promise { + const cmd = isWindows() ? '.cmd' : ''; + const packageManager = await getCurrentPackageManager(); + switch (packageManager) { + case 'npm': + return `npx${cmd}`; + case 'pnpm': + return `pnpx${cmd}`; + case 'yarn (berry)': + return `yarn${cmd} dlx`; + case 'yarn (classic)': + return `yarn${cmd}`; + default: + return `npx${cmd}`; + } +} + +const packageManagers = { + pnpm: 'pnpx', + 'yarn (berry)': 'yarn', + 'yarn (classic)': 'yarn', + yarn: 'yarn', + npm: 'npx', +}; + +export type PackageManager = keyof typeof packageManagers; + +export function getPackageManagerSpawnCommand( + pkgMng: keyof typeof packageManagers +): string { + const winCMD = isWindows() ? '.cmd' : ''; + return `${packageManagers[pkgMng]}${winCMD}`; +} + +function isWindows(): boolean { + return process.platform === 'win32'; +} diff --git a/src/cli.ts b/src/cli.ts index d9717de80..d656513b3 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,9 +5,12 @@ import { z } from 'zod'; import { argumentParser } from 'zodcli'; import type { ChalkInstance } from 'chalk'; import chalk from 'chalk'; -import { getCurrentPackageManager } from './buildApplication/getCurrentPackageManager'; -import { getSpawnCommand, nextOnPagesVersion } from './utils'; -import type { PackageManager } from './utils'; +import { nextOnPagesVersion } from './utils'; +import type { PackageManager } from './buildApplication/packageManagerUtils'; +import { + getCurrentPackageManager, + getPackageManagerSpawnCommand, +} from './buildApplication/packageManagerUtils'; // A helper type to handle command line flags. Defaults to false const flag = z @@ -219,7 +222,7 @@ export async function printEnvInfo(): Promise { function getPackageVersion(packageName: string): string { try { - const command = process.platform === 'win32' ? 'npm.cmd' : 'npm'; + const command = getPackageManagerSpawnCommand('npm'); const commandOutput = execFileSync( command, ['list', packageName, '--json', '--depth=0'], @@ -237,7 +240,7 @@ function getPackageVersion(packageName: string): string { function getBinaryVersion(binaryName: PackageManager): string { const commandArgs = ['--version']; try { - return execFileSync(getSpawnCommand(binaryName), commandArgs) + return execFileSync(getPackageManagerSpawnCommand(binaryName), commandArgs) .toString() .trim(); } catch { diff --git a/src/utils/getSpawnCommand.ts b/src/utils/getSpawnCommand.ts deleted file mode 100644 index ad2845133..000000000 --- a/src/utils/getSpawnCommand.ts +++ /dev/null @@ -1,14 +0,0 @@ -const packageManagers = { - pnpm: 'pnpx', - 'yarn (berry)': 'yarn', - 'yarn (classic)': 'yarn', - yarn: 'yarn', - npm: 'npx', -}; - -export type PackageManager = keyof typeof packageManagers; - -export function getSpawnCommand(pkgMng: keyof typeof packageManagers): string { - const winCMD = process.platform === 'win32' ? '.cmd' : ''; - return `${packageManagers[pkgMng]}${winCMD}`; -} diff --git a/src/utils/index.ts b/src/utils/index.ts index f52ea0d41..f397b8b1c 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,4 +1,3 @@ export * from './fs'; export * from './version'; -export * from './getSpawnCommand'; export * from './routing'; diff --git a/tests/src/buildApplication/getCurrentPackageManager.test.ts b/tests/src/buildApplication/getCurrentPackageManager.test.ts deleted file mode 100644 index a7b796365..000000000 --- a/tests/src/buildApplication/getCurrentPackageManager.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, vi, it, afterEach } from 'vitest'; -import { getCurrentPackageManager } from '../../../src/buildApplication/getCurrentPackageManager'; -import { EventEmitter } from 'events'; -import type { PackageManager } from '../../../src/utils'; - -let targetPkgMng: PackageManager = 'yarn (berry)'; - -describe('getCurrentPackageManager', async () => { - // yarn berry test environment - vi.stubEnv('npm_config_user_agent', 'yarn'); - vi.mock('child_process', async () => { - return { - spawn: () => { - const event = new EventEmitter() as EventEmitter & { - stdout: EventEmitter; - stderr: EventEmitter; - }; - event.stdout = new EventEmitter(); - event.stderr = new EventEmitter(); - setTimeout(() => { - event.stdout.emit( - 'data', - targetPkgMng === 'yarn (berry)' ? '3.0.0' : '1.0.0' - ); - event.emit('close', 0); - }, 100); - return event; - }, - }; - }); - vi.mock('fs/promises', async () => { - return { - stat: async () => null, - readFile: async () => `nodeLinker: node-modules`, - }; - }); - - afterEach(async () => { - vi.clearAllMocks(); - vi.unstubAllEnvs(); - if (targetPkgMng === 'yarn (berry)') { - // yarn classic test environment - vi.stubEnv('npm_config_user_agent', 'yarn'); - targetPkgMng = 'yarn (classic)'; - } else if (targetPkgMng === 'yarn (classic)') { - // pnpm test environment - vi.stubEnv('npm_config_user_agent', 'pnpm'); - targetPkgMng = 'pnpm'; - } else if (targetPkgMng === 'pnpm') { - // npm test environment - vi.stubEnv('npm_config_user_agent', 'npm'); - targetPkgMng = 'npm'; - } - }); - it('should detected yarn (berry)', async () => { - const pkgMng = await getCurrentPackageManager(); - expect(pkgMng).toEqual(targetPkgMng); - }); - it('should detected yarn (classic)', async () => { - const pkgMng = await getCurrentPackageManager(); - expect(pkgMng).toEqual(targetPkgMng); - }); - it('should detected pnpm', async () => { - const pkgMng = await getCurrentPackageManager(); - expect(pkgMng).toEqual(targetPkgMng); - }); - it('should detected npm', async () => { - const pkgMng = await getCurrentPackageManager(); - expect(pkgMng).toEqual(targetPkgMng); - }); -}); diff --git a/tests/src/buildApplication/packageManagerUtils.test.ts b/tests/src/buildApplication/packageManagerUtils.test.ts new file mode 100644 index 000000000..589d461f4 --- /dev/null +++ b/tests/src/buildApplication/packageManagerUtils.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, vi, it, afterAll } from 'vitest'; +import type { PackageManager } from '../../../src/buildApplication/packageManagerUtils'; +import { + getCurrentPackageExecuter, + getCurrentPackageManager, +} from '../../../src/buildApplication/packageManagerUtils'; +import { EventEmitter } from 'events'; + +describe('getCurrentPackageManager', async () => { + it('should detect yarn (berry)', async () => { + await testWith({ packageManager: 'yarn (berry)' }, async () => { + const pkgMng = await getCurrentPackageManager(); + expect(pkgMng).toEqual('yarn (berry)'); + }); + }); + it('should detect yarn (classic)', async () => { + await testWith({ packageManager: 'yarn (classic)' }, async () => { + const pkgMng = await getCurrentPackageManager(); + expect(pkgMng).toEqual('yarn (classic)'); + }); + }); + it('should detected pnpm', async () => { + await testWith({ packageManager: 'pnpm' }, async () => { + const pkgMng = await getCurrentPackageManager(); + expect(pkgMng).toEqual('pnpm'); + }); + }); + it('should detected npm', async () => { + await testWith({ packageManager: 'npm' }, async () => { + const pkgMng = await getCurrentPackageManager(); + expect(pkgMng).toEqual('npm'); + }); + }); +}); + +describe('getCurrentPackageExecuter', () => { + it('should detect yarn (berry)', async () => { + await testWith({ packageManager: 'yarn (berry)' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('yarn dlx'); + }); + }); + it('should detect yarn (classic)', async () => { + await testWith({ packageManager: 'yarn (classic)' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('yarn'); + }); + }); + it('should detected pnpm', async () => { + await testWith({ packageManager: 'pnpm' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('pnpx'); + }); + }); + it('should detected npm', async () => { + await testWith({ packageManager: 'npm' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('npx'); + }); + }); + + it('should detect yarn (berry) on windows', async () => { + await testWith( + { packageManager: 'yarn (berry)', os: 'windows' }, + async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('yarn.cmd dlx'); + } + ); + }); + it('should detect yarn (classic) on windows', async () => { + await testWith( + { packageManager: 'yarn (classic)', os: 'windows' }, + async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('yarn.cmd'); + } + ); + }); + it('should detected pnpm on windows', async () => { + await testWith({ packageManager: 'pnpm', os: 'windows' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('pnpx.cmd'); + }); + }); + it('should detected npm on windows', async () => { + await testWith({ packageManager: 'npm', os: 'windows' }, async () => { + const pkgMng = await getCurrentPackageExecuter(); + expect(pkgMng).toEqual('npx.cmd'); + }); + }); +}); + +async function testWith( + { + packageManager, + os = 'linux/macos', + }: { + packageManager: Exclude; + os?: 'windows' | 'linux/macos'; + }, + test: () => Promise +): Promise { + currentMocks.packageManager = packageManager; + currentMocks.os = os; + vi.stubEnv('npm_config_user_agent', packageManager); + vi.spyOn(process, 'platform', 'get').mockReturnValue( + currentMocks.os === 'windows' ? 'win32' : 'linux' + ); + await test(); + vi.unstubAllEnvs(); +} + +const currentMocks: { + packageManager: Exclude; + os: 'windows' | 'linux/macos'; +} = { + packageManager: 'npm', + os: 'windows', +}; + +vi.mock('fs/promises', async () => { + return { + stat: async () => null, + readFile: async () => `nodeLinker: node-modules`, + }; +}); + +vi.mock('child_process', async () => { + return { + spawn: () => { + const event = new EventEmitter() as EventEmitter & { + stdout: EventEmitter; + stderr: EventEmitter; + }; + event.stdout = new EventEmitter(); + event.stderr = new EventEmitter(); + setTimeout(() => { + event.stdout.emit( + 'data', + currentMocks.packageManager === 'yarn (berry)' ? '3.0.0' : '1.0.0' + ); + event.emit('close', 0); + }, 100); + return event; + }, + }; +}); + +afterAll(async () => { + vi.clearAllMocks(); +});