Skip to content
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

Added nodejs-npm-install Buildpack #625

Merged
merged 36 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d436eb2
Added nodejs-npm-install Buildpack
colincasey Aug 16, 2023
88d4903
Merge branch 'main' into npm-buildpacks/nodejs-npm-install
colincasey Sep 7, 2023
3597967
Refactoring + output logging and error messages
colincasey Sep 8, 2023
bbf371c
Refactoring + output logging and error messages
colincasey Sep 8, 2023
5496b14
Refactoring + output logging and error messages
colincasey Sep 8, 2023
40dc2bc
Added README.md
colincasey Sep 29, 2023
aca3682
Added new line to buildpack.toml
colincasey Sep 29, 2023
99c3765
Added ending newline to several files
colincasey Sep 29, 2023
2728a94
Fix link
colincasey Sep 29, 2023
73ef5ad
Fix typo
colincasey Sep 29, 2023
27473cf
Merge branch 'main' into npm-buildpacks/nodejs-npm-install
colincasey Oct 4, 2023
d7b8993
Add npm install w/ no package-lock.json
colincasey Oct 4, 2023
4b05d74
Fix lint error
colincasey Oct 4, 2023
9912f70
Don't use -s flag when executing scripts
colincasey Oct 6, 2023
f141c2b
Fix typo in multiple lockfile error and include link
colincasey Oct 6, 2023
39e20f1
Include text from CX error review
colincasey Oct 11, 2023
3049052
Include text from CX error review
colincasey Oct 11, 2023
f3a9ff5
Merge branch 'main' into npm-buildpacks/nodejs-npm-install
colincasey Oct 11, 2023
cee513a
Include text from CX error review
colincasey Oct 11, 2023
fe58644
Fix tests
colincasey Oct 11, 2023
bac2552
Update buildpacks/nodejs-npm-install/buildpack.toml
colincasey Oct 13, 2023
5c1de0d
Update buildpacks/nodejs-npm-install/CHANGELOG.md
colincasey Oct 13, 2023
0d38455
Update buildpacks/nodejs-npm-install/buildpack.toml
colincasey Oct 13, 2023
54943f1
Added try_exist for package.json detection and updated errors
colincasey Oct 13, 2023
f669a1b
Fixed buildpack name
colincasey Oct 16, 2023
64ee342
Update buildpacks/nodejs-npm-install/src/errors.rs
colincasey Oct 16, 2023
99f30bc
Update buildpacks/nodejs-npm-install/src/errors.rs
colincasey Oct 16, 2023
fcb608a
Update buildpacks/nodejs-npm-install/README.md
colincasey Oct 16, 2023
0543ca2
More error corrections.
colincasey Oct 16, 2023
a84f802
Require package-lock to detect and remove support for install with no…
colincasey Oct 17, 2023
1b66b4d
Add package-lock.json to test fixtures
colincasey Oct 17, 2023
e06fcad
Adding exit status clarification
colincasey Oct 17, 2023
07a67c8
Merge branch 'main' into npm-buildpacks/nodejs-npm-install
colincasey Oct 17, 2023
3588650
Update buildpack.toml
colincasey Oct 17, 2023
0099bef
Merge branch 'main' into npm-buildpacks/nodejs-npm-install
colincasey Oct 17, 2023
e86466e
Update lockfile
colincasey Oct 17, 2023
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
290 changes: 279 additions & 11 deletions Cargo.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"buildpacks/nodejs-engine",
"buildpacks/nodejs-corepack",
"buildpacks/nodejs-function-invoker",
"buildpacks/nodejs-npm-install",
"buildpacks/nodejs-pnpm-install",
"buildpacks/nodejs-yarn",
"common/nodejs-utils",
Expand All @@ -16,11 +17,13 @@ edition = "2021"
publish = false

[workspace.dependencies]
commons = { git = "https://github.com/heroku/buildpacks-ruby", branch = "schneems/logging-state-machine-continued" }
heroku-nodejs-utils = { path = "./common/nodejs-utils" }
indoc = "2"
# libcnb has a much bigger impact on buildpack behaviour than any other dependencies,
# so it's pinned to an exact version to isolate it from lockfile refreshes.
libcnb-test = "=0.14.0"
libcnb-data = { git = "https://github.com/heroku/libcnb.rs", branch = "libcnb_test_meta_buildpack_support" }
libcnb-test = { git = "https://github.com/heroku/libcnb.rs", branch = "libcnb_test_meta_buildpack_support" }
libcnb = "=0.14.0"
libherokubuildpack = "=0.14.0"
serde = "1"
Expand Down
6 changes: 5 additions & 1 deletion buildpacks/nodejs-engine/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ impl Buildpack for NodeJsEngineBuildpack {
type Error = NodeJsEngineBuildpackError;

fn detect(&self, context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
let mut plan_builder = BuildPlanBuilder::new().provides("node");
let mut plan_builder = BuildPlanBuilder::new()
.provides("node")
.provides("npm")
.or()
.provides("node");

// If there are common node artifacts, this buildpack should both
// provide and require node so that it may be used without other
Expand Down
10 changes: 10 additions & 0 deletions buildpacks/nodejs-npm-install/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

- Initial release
22 changes: 22 additions & 0 deletions buildpacks/nodejs-npm-install/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[package]
name = "heroku-npm-install-buildpack"
description = "Heroku Node.js npm Install Cloud Native Buildpack"
version.workspace = true
rust-version.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
commons.workspace = true
heroku-nodejs-utils.workspace = true
libcnb.workspace = true
libherokubuildpack.workspace = true
serde.workspace = true
indoc.workspace = true
toml.workspace = true

[dev-dependencies]
libcnb-test.workspace = true
serde_json.workspace = true
test_support.workspace = true
ureq.workspace = true
1 change: 1 addition & 0 deletions buildpacks/nodejs-npm-install/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO
28 changes: 28 additions & 0 deletions buildpacks/nodejs-npm-install/buildpack.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
api = "0.9"

[buildpack]
id = "heroku/nodejs-npm-install"
version = "1.1.4"
name = "Heroku Node.js npm Install"
homepage = "https://github.com/heroku/buildpacks-nodejs"
keywords = ["node", "node.js", "nodejs", "javascript", "js", "npm", "install"]

[[buildpack.licenses]]
type = "MIT"

[[stacks]]
id = "*"

[[stacks]]
id = "heroku-20"

[[stacks]]
id = "heroku-22"

[[stacks]]
id = "io.buildpacks.stacks.bionic"

colincasey marked this conversation as resolved.
Show resolved Hide resolved
[metadata]
[metadata.release]
[metadata.release.docker]
colincasey marked this conversation as resolved.
Show resolved Hide resolved
repository = "docker.io/heroku/buildpack-nodejs-npm-install"
169 changes: 169 additions & 0 deletions buildpacks/nodejs-npm-install/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
use crate::npm;
use commons::fun_run::CmdError;
use commons::output::build_log::{BuildLog, Logger, StartedLogger};
use commons::output::fmt::DEBUG_INFO;
use heroku_nodejs_utils::application;
use heroku_nodejs_utils::package_json::PackageJsonError;
use indoc::formatdoc;
use std::fmt::Display;
use std::io::stdout;

const OPEN_A_SUPPORT_TICKET: &str =
"open a support ticket and include the full log output of this build";

const TRY_BUILDING_AGAIN: &str = "try again and to see if the error resolves itself";

#[derive(Debug)]
pub(crate) enum NpmInstallBuildpackError {
Application(application::Error),
BuildScript(CmdError),
NpmInstall(CmdError),
NpmSetCacheDir(CmdError),
NpmVersion(npm::VersionError),
PackageJson(PackageJsonError),
}

pub(crate) fn on_error(error: libcnb::Error<NpmInstallBuildpackError>) {
let logger = BuildLog::new(stdout()).without_buildpack_name();
match error {
libcnb::Error::BuildpackError(buildpack_error) => {
on_buildpack_error(buildpack_error, logger)
}
framework_error => on_framework_error(framework_error, logger),
}
}

pub(crate) fn on_buildpack_error(error: NpmInstallBuildpackError, logger: Box<dyn StartedLogger>) {
match error {
NpmInstallBuildpackError::PackageJson(e) => on_package_json_error(e, logger),
NpmInstallBuildpackError::NpmSetCacheDir(e) => on_set_cache_dir_error(e, logger),
NpmInstallBuildpackError::NpmVersion(e) => on_npm_version_error(e, logger),
NpmInstallBuildpackError::NpmInstall(e) => on_npm_install_error(e, logger),
NpmInstallBuildpackError::BuildScript(e) => on_build_script_error(e, logger),
NpmInstallBuildpackError::Application(e) => on_application_error(e, logger),
}
}

fn on_package_json_error(error: PackageJsonError, logger: Box<dyn StartedLogger>) {
match error {
PackageJsonError::AccessError(e) => {
print_error_details(logger, e)
.announce()
.error(&formatdoc! {"
Failed to read package.json

An unexpected error occurred while reading package.json.

Please {TRY_BUILDING_AGAIN}.
"});
}
PackageJsonError::ParseError(e) => {
print_error_details(logger, e)
.announce()
.error(&formatdoc! {"
Failed to parse package.json

An unexpected error occurred while parsing package.json.

Please {TRY_BUILDING_AGAIN}.
colincasey marked this conversation as resolved.
Show resolved Hide resolved
"});
}
}
}

fn on_set_cache_dir_error(error: CmdError, logger: Box<dyn StartedLogger>) {
let command = error.name().to_string();
print_error_details(logger, error)
.announce()
.error(&formatdoc! {"
Failed to set the npm cache directory

An unexpected error occurred while executing `{command}`.

Please {TRY_BUILDING_AGAIN}.
"});
}

fn on_npm_version_error(error: npm::VersionError, logger: Box<dyn StartedLogger>) {
match error {
npm::VersionError::Command(e) => {
let command = e.name().to_string();
print_error_details(logger, e)
.announce()
.error(&formatdoc! {"
Failed to determine npm version information

An unexpected error occurred while executing `{command}`.

Please {TRY_BUILDING_AGAIN}.
"});
}
npm::VersionError::Parse(e) => {
logger.announce().error(&formatdoc! {"
Failed to parse npm version information

An unexpected error occurred while parsing version information from `{e}`.

Please {TRY_BUILDING_AGAIN}.
colincasey marked this conversation as resolved.
Show resolved Hide resolved
"});
}
}
}

fn on_npm_install_error(error: CmdError, logger: Box<dyn StartedLogger>) {
let command = error.name().to_string();
print_error_details(logger, error)
.announce()
.error(&formatdoc! {"
Failed to install node modules

An unexpected error occurred while executing `{command}`. See the log output above for more information.

In some cases, this happens due to an unstable network connection. Please {TRY_BUILDING_AGAIN}.

If that does not help, check the status of npm (the upstream Node module repository service) here:
https://status.npmjs.org/
"});
}

fn on_build_script_error(error: CmdError, logger: Box<dyn StartedLogger>) {
let command = error.name().to_string();
print_error_details(logger, error)
.announce()
.error(&formatdoc! {"
Failed to execute build script

An unexpected error occurred while executing `{command}`.

Please try running this command locally to verify that it works as expected.
"});
}

fn on_application_error(error: application::Error, logger: Box<dyn StartedLogger>) {
logger.announce().error(&error.to_string());
}

fn on_framework_error(
error: libcnb::Error<NpmInstallBuildpackError>,
logger: Box<dyn StartedLogger>,
) {
print_error_details(logger, error)
.announce()
.error(&formatdoc! {"
Internal buildpack error

An unexpected internal error was reported by the framework used by this buildpack.

Please {OPEN_A_SUPPORT_TICKET}.
"});
}

fn print_error_details(
logger: Box<dyn StartedLogger>,
error: impl Display,
) -> Box<dyn StartedLogger> {
logger
.section(DEBUG_INFO)
.step(&error.to_string())
.end_section()
}
1 change: 1 addition & 0 deletions buildpacks/nodejs-npm-install/src/layers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub(crate) mod npm_cache;
64 changes: 64 additions & 0 deletions buildpacks/nodejs-npm-install/src/layers/npm_cache.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use crate::NpmInstallBuildpack;
use commons::output::section_log::{log_step, SectionLogger};
use libcnb::build::BuildContext;
use libcnb::data::layer_content_metadata::LayerTypes;
use libcnb::layer::{ExistingLayerStrategy, Layer, LayerData, LayerResult, LayerResultBuilder};
use libcnb::Buildpack;
use serde::{Deserialize, Serialize};
use std::path::Path;

pub(crate) struct NpmCacheLayer<'a> {
// this ensures we have a logging section already created
pub(crate) _section_logger: &'a dyn SectionLogger,
}

impl<'a> Layer for NpmCacheLayer<'a> {
type Buildpack = NpmInstallBuildpack;
type Metadata = NpmCacheLayerMetadata;

fn types(&self) -> LayerTypes {
LayerTypes {
build: true,
launch: false,
cache: true,
}
}

fn create(
&self,
_context: &BuildContext<Self::Buildpack>,
_layer_path: &Path,
) -> Result<LayerResult<Self::Metadata>, <Self::Buildpack as Buildpack>::Error> {
log_step("Creating npm cache");
LayerResultBuilder::new(NpmCacheLayerMetadata::default()).build()
}

fn existing_layer_strategy(
&self,
_context: &BuildContext<Self::Buildpack>,
layer_data: &LayerData<Self::Metadata>,
) -> Result<ExistingLayerStrategy, <Self::Buildpack as Buildpack>::Error> {
if layer_data.content_metadata.metadata.layer_version == LAYER_VERSION {
log_step("Restoring npm cache");
Ok(ExistingLayerStrategy::Keep)
} else {
log_step("Recreating npm cache (layer version changed)");
Ok(ExistingLayerStrategy::Recreate)
}
}
}

const LAYER_VERSION: &str = "1";

#[derive(Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct NpmCacheLayerMetadata {
layer_version: String,
}

impl Default for NpmCacheLayerMetadata {
fn default() -> Self {
Self {
layer_version: LAYER_VERSION.to_string(),
}
}
}
Loading