Skip to content

Improve pnpm engine errors #1077

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions buildpacks/nodejs-pnpm-engine/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Changed

- Updated error messages and formatting. ([#1077](https://github.com/heroku/buildpacks-nodejs/pull/1077))

## [3.6.0] - 2025-04-09

- No changes.
Expand Down
2 changes: 2 additions & 0 deletions buildpacks/nodejs-pnpm-engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ workspace = true

[dependencies]
bullet_stream = "0.7"
heroku-nodejs-utils.workspace = true
indoc = "2"
libcnb = { version = "=0.28.1", features = ["trace"] }

[dev-dependencies]
insta = "1"
libcnb-test = "=0.28.1"
test_support.workspace = true
123 changes: 68 additions & 55 deletions buildpacks/nodejs-pnpm-engine/src/errors.rs
Original file line number Diff line number Diff line change
@@ -1,81 +1,94 @@
use crate::BUILDPACK_NAME;
use bullet_stream::state::Bullet;
use bullet_stream::{style, Print};
use crate::PnpmEngineBuildpackError;
use bullet_stream::style;
use heroku_nodejs_utils::error_handling::error_message_builder::SetIssuesUrl;
use heroku_nodejs_utils::error_handling::{
on_framework_error, ErrorMessage, ErrorMessageBuilder, ErrorType, SuggestRetryBuild,
SuggestSubmitIssue,
};
use indoc::formatdoc;
use std::fmt::Display;
use std::io::{stderr, Stderr};

#[derive(Debug, Copy, Clone)]
pub(crate) enum PnpmEngineBuildpackError {
CorepackRequired,
}
const BUILDPACK_NAME: &str = "Heroku Node.js pnpm Engine";

const ISSUES_URL: &str = "https://github.com/heroku/buildpacks-nodejs/issues";

pub(crate) fn on_error(error: libcnb::Error<PnpmEngineBuildpackError>) {
let logger = Print::new(stderr()).without_header();
pub(crate) fn on_error(error: libcnb::Error<PnpmEngineBuildpackError>) -> ErrorMessage {
match error {
libcnb::Error::BuildpackError(buildpack_error) => {
on_buildpack_error(buildpack_error, logger);
}
framework_error => on_framework_error(&framework_error, logger),
libcnb::Error::BuildpackError(e) => on_buildpack_error(e),
e => on_framework_error(BUILDPACK_NAME, ISSUES_URL, &e),
}
}

fn on_buildpack_error(error: PnpmEngineBuildpackError, logger: Print<Bullet<Stderr>>) {
// Wraps the error_message() builder to preset the issues_url field
fn error_message() -> ErrorMessageBuilder<SetIssuesUrl> {
heroku_nodejs_utils::error_handling::error_message().issues_url(ISSUES_URL.to_string())
}

fn on_buildpack_error(error: PnpmEngineBuildpackError) -> ErrorMessage {
match error {
PnpmEngineBuildpackError::CorepackRequired => {
print_error_details(logger, &"Corepack Requirement Error").error(formatdoc! {"
A pnpm lockfile ({pnpm_lockfile}) was detected, but the
let corepack_enable = style::command("corepack enable");
let corepack_use_pnpm = style::command("corepack use pnpm@*");
let heroku_nodejs_corepack = style::command("heroku/nodejs-corepack");
let package_manager = style::value("packageManager");
let pnpm = style::value("pnpm");
let pnpm_lockfile = style::value("pnpm-lock.yaml");
let package_json = style::value("package.json");

error_message()
.error_type(ErrorType::UserFacing(
SuggestRetryBuild::No,
SuggestSubmitIssue::No,
))
.header("Corepack Requirement Error")
.body(formatdoc! {"
A pnpm lockfile ({pnpm_lockfile}) was detected, but the \
version of {pnpm} to install could not be determined.

{pnpm} may be installed via the {heroku_nodejs_corepack}
buildpack. It requires the desired {pnpm} version to be set
{pnpm} may be installed via the {heroku_nodejs_corepack} \
buildpack. It requires the desired {pnpm} version to be set \
via the {package_manager} key in {package_json}.

To set {package_manager} in {package_json} to the latest
To set {package_manager} in {package_json} to the latest \
{pnpm}, run:

{corepack_enable}
{corepack_use_pnpm}

Then commit the result, and try again.
",
corepack_enable = style::command("corepack enable"),
corepack_use_pnpm = style::command("corepack use pnpm@*"),
heroku_nodejs_corepack = style::command("heroku/nodejs-corepack"),
package_manager = style::value("packageManager"),
pnpm = style::value("pnpm"),
pnpm_lockfile = style::value("pnpm-lock.yaml"),
package_json = style::value("package.json")});
" })
.create()
}
}
}

fn on_framework_error(
error: &libcnb::Error<PnpmEngineBuildpackError>,
logger: Print<Bullet<Stderr>>,
) {
print_error_details(logger, &error)
.error(formatdoc! {"
{buildpack_name} internal error.
#[cfg(test)]
mod tests {
use super::*;
use bullet_stream::strip_ansi;
use insta::{assert_snapshot, with_settings};
use libcnb::Error;
use test_support::test_name;

The framework used by this buildpack encountered an unexpected error.

If you can't deploy to Heroku due to this issue, check the official Heroku Status page at \
status.heroku.com for any ongoing incidents. After all incidents resolve, retry your build.

If the issue persists and you think you found a bug in the buildpack or framework, reproduce \
the issue locally with a minimal example. Open an issue in the buildpack's GitHub repository \
and include the details.

", buildpack_name = style::value(BUILDPACK_NAME) });
}
#[test]
fn test_pnpm_engine_corepack_required_error() {
assert_error_snapshot(PnpmEngineBuildpackError::CorepackRequired);
}

fn print_error_details(
logger: Print<Bullet<Stderr>>,
error: &impl Display,
) -> Print<Bullet<Stderr>> {
logger
.bullet(style::important("DEBUG INFO:"))
.sub_bullet(error.to_string())
.done()
fn assert_error_snapshot(error: impl Into<Error<PnpmEngineBuildpackError>>) {
let error_message = strip_ansi(on_error(error.into()).to_string());
let test_name = format!(
"errors__{}",
test_name()
.split("::")
.last()
.unwrap()
.trim_start_matches("test")
);
with_settings!({
prepend_module_to_snapshot => false,
omit_expression => true,
}, {
assert_snapshot!(test_name, error_message);
});
}
}
22 changes: 17 additions & 5 deletions buildpacks/nodejs-pnpm-engine/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@

mod errors;

use crate::errors::PnpmEngineBuildpackError;
use bullet_stream::Print;
use libcnb::build::{BuildContext, BuildResult};
use libcnb::data::build_plan::BuildPlanBuilder;
use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
use libcnb::generic::{GenericMetadata, GenericPlatform};
use libcnb::{buildpack_main, Buildpack};
#[cfg(test)]
use libcnb_test as _;
use std::io::stderr;
#[cfg(test)]
use test_support as _;

const BUILDPACK_NAME: &str = "Heroku Node.js pnpm Engine Buildpack";

struct PnpmEngineBuildpack;

impl Buildpack for PnpmEngineBuildpack {
Expand All @@ -40,17 +39,30 @@ impl Buildpack for PnpmEngineBuildpack {
DetectResultBuilder::fail().build()
}

fn build(&self, _context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {
let _logger = Print::new(stderr()).h1(context
.buildpack_descriptor
.buildpack
.name
.as_ref()
.expect("The buildpack.toml should have a 'name' field set"));

// This buildpack does not install pnpm yet, suggest using
// `heroku/nodejs-corepack` instead.
Err(PnpmEngineBuildpackError::CorepackRequired)?
}

fn on_error(&self, error: libcnb::Error<Self::Error>) {
errors::on_error(error);
let error_message = errors::on_error(error);
eprintln!("\n{error_message}");
}
}

#[derive(Debug, Copy, Clone)]
pub(crate) enum PnpmEngineBuildpackError {
CorepackRequired,
}

impl From<PnpmEngineBuildpackError> for libcnb::Error<PnpmEngineBuildpackError> {
fn from(value: PnpmEngineBuildpackError) -> Self {
libcnb::Error::BuildpackError(value)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
source: buildpacks/nodejs-pnpm-engine/src/errors.rs
---
! Corepack Requirement Error
!
! A pnpm lockfile (`pnpm-lock.yaml`) was detected, but the version of `pnpm` to install could not be determined.
!
! `pnpm` may be installed via the `heroku/nodejs-corepack` buildpack. It requires the desired `pnpm` version to be set via the `packageManager` key in `package.json`.
!
! To set `packageManager` in `package.json` to the latest `pnpm`, run:
!
! `corepack enable`
! `corepack use pnpm@*`
!
! Then commit the result, and try again.
30 changes: 15 additions & 15 deletions buildpacks/nodejs-pnpm-engine/tests/integration_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,21 @@ fn pnpm_unknown_version() {
assert_contains!(
ctx.pack_stderr,
&formatdoc! {"
! A pnpm lockfile (`pnpm-lock.yaml`) was detected, but the
! version of `pnpm` to install could not be determined.
!
! `pnpm` may be installed via the `heroku/nodejs-corepack`
! buildpack. It requires the desired `pnpm` version to be set
! via the `packageManager` key in `package.json`.
!
! To set `packageManager` in `package.json` to the latest
! `pnpm`, run:
!
! `corepack enable`
! `corepack use pnpm@*`
!
! Then commit the result, and try again.
"}
! A pnpm lockfile (`pnpm-lock.yaml`) was detected, but the \
version of `pnpm` to install could not be determined.
!
! `pnpm` may be installed via the `heroku/nodejs-corepack` \
buildpack. It requires the desired `pnpm` version to be set \
via the `packageManager` key in `package.json`.
!
! To set `packageManager` in `package.json` to the latest \
`pnpm`, run:
!
! `corepack enable`
! `corepack use pnpm@*`
!
! Then commit the result, and try again.
"}
);
},
);
Expand Down