From 0b9fc8c0d409d52e11f66352d33d32a8aff15b14 Mon Sep 17 00:00:00 2001 From: Colin Casey Date: Tue, 22 Oct 2024 15:58:56 -0300 Subject: [PATCH] Allow participating buildpacks to opt-out of Node.js build scripts (#928) * Allow participating buildpacks to opt-out of Node.js build scripts The buildpacks that performs installation for each of the supported package managers (npm, pnpm, Yarn) will execute a preset list of scripts from `package.json` if present: - `heroku-prebuild` - `heroku-build` or `build` - `heroku-postbuild` This PR adds a new Buildpack Plan called `node_build_scripts` to the package manager installation buildpacks that can be configured by later buildpacks that require it with the following: ```toml [[requires]] name = "node_build_scripts" [requires.metadata] enabled = ``` * Allow participating buildpacks to opt-out of Node.js build scripts The buildpacks that performs installation for each of the supported package managers (npm, pnpm, Yarn) will execute a preset list of scripts from `package.json` if present: - `heroku-prebuild` - `heroku-build` or `build` - `heroku-postbuild` This PR adds a new Buildpack Plan called `node_build_scripts` to the package manager installation buildpacks that can be configured by later buildpacks that require it with the following: ```toml [[requires]] name = "node_build_scripts" [requires.metadata] enabled = ``` * Update CHANGELOG.md * Update CHANGELOG.md * Update CHANGELOG.md * Update README.md * Added yarn-project which for some reason didn't get added in the previous commits * Update Cargo.lock after merge --- Cargo.lock | 80 +++++++++++ buildpacks/nodejs-npm-install/CHANGELOG.md | 4 + buildpacks/nodejs-npm-install/README.md | 36 +++-- buildpacks/nodejs-npm-install/src/errors.rs | 27 ++++ buildpacks/nodejs-npm-install/src/main.rs | 44 ++++-- .../tests/integration_test.rs | 57 +++++++- buildpacks/nodejs-pnpm-install/CHANGELOG.md | 4 + buildpacks/nodejs-pnpm-install/README.md | 50 ++++++- buildpacks/nodejs-pnpm-install/src/errors.rs | 30 +++- buildpacks/nodejs-pnpm-install/src/main.rs | 23 ++- .../tests/fixtures/pnpm-9/package.json | 9 ++ .../tests/fixtures/pnpm-9/pnpm-lock.yaml | 23 +++ .../tests/integration_test.rs | 59 +++++++- buildpacks/nodejs-yarn/CHANGELOG.md | 4 + buildpacks/nodejs-yarn/Cargo.toml | 1 + buildpacks/nodejs-yarn/README.md | 56 ++++++-- buildpacks/nodejs-yarn/src/main.rs | 29 +++- .../tests/fixtures/yarn-project/.editorconfig | 10 ++ .../fixtures/yarn-project/.gitattributes | 4 + .../tests/fixtures/yarn-project/.gitignore | 13 ++ .../tests/fixtures/yarn-project/README.md | 1 + .../tests/fixtures/yarn-project/package.json | 5 + .../tests/fixtures/yarn-project/yarn.lock | 12 ++ .../nodejs-yarn/tests/integration_test.rs | 58 +++++++- common/nodejs-utils/Cargo.toml | 1 + common/nodejs-utils/src/buildplan.rs | 133 ++++++++++++++++++ common/nodejs-utils/src/lib.rs | 1 + test_support/Cargo.toml | 2 + test_support/src/lib.rs | 40 +++++- 29 files changed, 759 insertions(+), 57 deletions(-) create mode 100644 buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/package.json create mode 100644 buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/pnpm-lock.yaml create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.editorconfig create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitattributes create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitignore create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/README.md create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/package.json create mode 100644 buildpacks/nodejs-yarn/tests/fixtures/yarn-project/yarn.lock create mode 100644 common/nodejs-utils/src/buildplan.rs diff --git a/Cargo.lock b/Cargo.lock index 5f19b767..d899da10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,29 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bon" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97493a391b4b18ee918675fb8663e53646fd09321c58b46afa04e8ce2499c869" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2af3eac944c12cdf4423eab70d310da0a8e5851a18ffb192c0a5e3f7ae1663" +dependencies = [ + "darling", + "ident_case", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "bstr" version = "1.10.0" @@ -314,6 +337,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "digest" version = "0.10.7" @@ -635,6 +693,7 @@ dependencies = [ "heroku-inventory-utils", "indoc", "keep_a_changelog_file", + "libcnb-data", "node-semver", "opentelemetry 0.24.0", "opentelemetry-stdout 0.5.0", @@ -656,6 +715,7 @@ name = "heroku-nodejs-yarn-buildpack" version = "0.0.0" dependencies = [ "heroku-nodejs-utils", + "indoc", "libcnb", "libcnb-test", "libherokubuildpack 0.23.0", @@ -748,6 +808,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.5.0" @@ -1374,6 +1440,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + [[package]] name = "ryu" version = "1.0.18" @@ -1483,6 +1555,12 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -1536,9 +1614,11 @@ dependencies = [ name = "test_support" version = "0.0.0" dependencies = [ + "bon", "libcnb", "libcnb-test", "serde_json", + "tempfile", "ureq", ] diff --git a/buildpacks/nodejs-npm-install/CHANGELOG.md b/buildpacks/nodejs-npm-install/CHANGELOG.md index dcd6d602..a52f87e5 100644 --- a/buildpacks/nodejs-npm-install/CHANGELOG.md +++ b/buildpacks/nodejs-npm-install/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow configuration of build script behavior through the `node_build_scripts` build plan. ([#928](https://github.com/heroku/buildpacks-nodejs/pull/928)) + ## [3.2.15] - 2024-10-04 - No changes. diff --git a/buildpacks/nodejs-npm-install/README.md b/buildpacks/nodejs-npm-install/README.md index 87a1895b..2f3df2b1 100644 --- a/buildpacks/nodejs-npm-install/README.md +++ b/buildpacks/nodejs-npm-install/README.md @@ -36,20 +36,37 @@ added that executes `npm start`. ## Build Plan +### Provides + +| Name | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------| +| `node_modules` | Allows other buildpacks to depend on the Node modules provided by this buildpack. | +| `node_build_scripts` | Allows other buildpacks to depend on the [build script execution](#step-3-execute-build-scripts) behavior provided by this buildpack. | + ### Requires -| Name | Description | -|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `node` | To execute `npm` a [Node.js][Node.js] runtime is required. It can be provided by the [`heroku/nodejs-engine`][heroku/nodejs-engine] buildpack. | -| `npm` | To install node modules, the [npm][npm] package manager is required. It can be provided by either the [`heroku/nodejs-engine`][heroku/nodejs-engine] or [`heroku/nodejs-npm-engine`][heroku/nodejs-npm-engine] buildpacks. | -| `node_modules` | This is not a strict requirement of the buildpack. Requiring `node_modules` ensures that this buildpack can be used even when no other buildpack requires `node_modules`. | +| Name | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `node` | To execute `npm` a [Node.js][Node.js] runtime is required. It can be provided by the [`heroku/nodejs-engine`][heroku/nodejs-engine] buildpack. | +| `npm` | To install node modules, the [npm][npm] package manager is required. It can be provided by either the [`heroku/nodejs-engine`][heroku/nodejs-engine] or [`heroku/nodejs-npm-engine`][heroku/nodejs-npm-engine] buildpacks. | +| `node_modules` | This is not a strict requirement of the buildpack. Requiring `node_modules` ensures that this buildpack can be used even when no other buildpack requires `node_modules`. | +| `node_build_scripts` | This is not a strict requirement of the buildpack. Requiring `node_build_scripts` ensures that this buildpack will perform [build script execution](#step-3-execute-build-scripts) even when no other buildpack requires `node_build_scripts`. | | +#### Build Plan Metadata Schemas -### Provides +##### `node_build_scripts` + +* `enabled` ([boolean][toml_type_boolean], optional) + +###### Example + +```toml +[[requires]] +name = "node_build_scripts" -| Name | Description | -|----------------|-----------------------------------------------------------------------------------| -| `node_modules` | Allows other buildpacks to depend on the Node modules provided by this buildpack. | +[requires.metadata] +enabled = false # this will prevent build scripts from running +``` ## License @@ -63,3 +80,4 @@ See [LICENSE](../../LICENSE) file. [npm]: https://docs.npmjs.com/ [heroku/nodejs-engine]: ../nodejs-engine/README.md [heroku/nodejs-npm-engine]: ../nodejs-npm-engine/README.md +[toml_type_boolean]: https://toml.io/en/v1.0.0#boolean diff --git a/buildpacks/nodejs-npm-install/src/errors.rs b/buildpacks/nodejs-npm-install/src/errors.rs index b31d71c9..a717d62f 100644 --- a/buildpacks/nodejs-npm-install/src/errors.rs +++ b/buildpacks/nodejs-npm-install/src/errors.rs @@ -5,6 +5,9 @@ use commons::output::fmt; use commons::output::fmt::DEBUG_INFO; use fun_run::CmdError; use heroku_nodejs_utils::application; +use heroku_nodejs_utils::buildplan::{ + NodeBuildScriptsMetadataError, NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME, +}; use heroku_nodejs_utils::package_json::PackageJsonError; use indoc::formatdoc; use std::fmt::Display; @@ -27,6 +30,7 @@ pub(crate) enum NpmInstallBuildpackError { NpmSetCacheDir(CmdError), NpmVersion(npm::VersionError), PackageJson(PackageJsonError), + NodeBuildScriptsMetadata(NodeBuildScriptsMetadataError), } pub(crate) fn on_error(error: libcnb::Error) { @@ -44,6 +48,9 @@ fn on_buildpack_error(error: NpmInstallBuildpackError, logger: Box on_application_error(&e, logger), NpmInstallBuildpackError::BuildScript(e) => on_build_script_error(&e, logger), NpmInstallBuildpackError::Detect(e) => on_detect_error(&e, logger), + NpmInstallBuildpackError::NodeBuildScriptsMetadata(e) => { + on_node_build_scripts_metadata_error(e, logger); + } NpmInstallBuildpackError::NpmInstall(e) => on_npm_install_error(&e, logger), NpmInstallBuildpackError::NpmSetCacheDir(e) => on_set_cache_dir_error(&e, logger), NpmInstallBuildpackError::NpmVersion(e) => on_npm_version_error(e, logger), @@ -51,6 +58,26 @@ fn on_buildpack_error(error: NpmInstallBuildpackError, logger: Box, +) { + let NodeBuildScriptsMetadataError::InvalidEnabledValue(value) = error; + let value_type = value.type_str(); + logger.announce().error(&formatdoc! { " + A participating buildpack has set invalid `[requires.metadata]` for the build plan \ + named `{NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME}`. + + Expected metadata format: + [requires.metadata] + enabled = + + But was: + [requires.metadata] + enabled = <{value_type}> + "}); +} + fn on_package_json_error(error: PackageJsonError, logger: Box) { match error { PackageJsonError::AccessError(e) => { diff --git a/buildpacks/nodejs-npm-install/src/main.rs b/buildpacks/nodejs-npm-install/src/main.rs index 1f2ff442..b50aa1e6 100644 --- a/buildpacks/nodejs-npm-install/src/main.rs +++ b/buildpacks/nodejs-npm-install/src/main.rs @@ -12,6 +12,9 @@ use commons::output::section_log::{log_step, log_step_stream}; use commons::output::warn_later::WarnGuard; use fun_run::{CommandWithName, NamedOutput}; use heroku_nodejs_utils::application; +use heroku_nodejs_utils::buildplan::{ + read_node_build_scripts_metadata, NodeBuildScriptsMetadata, NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME, +}; use heroku_nodejs_utils::package_json::PackageJson; use heroku_nodejs_utils::package_manager::PackageManager; use heroku_nodejs_utils::vrs::Version; @@ -53,9 +56,11 @@ impl Buildpack for NpmInstallBuildpack { .build_plan( BuildPlanBuilder::new() .provides("node_modules") + .provides(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) + .requires("node") .requires("npm") .requires("node_modules") - .requires("node") + .requires(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) .build(), ) .build() @@ -74,6 +79,8 @@ impl Buildpack for NpmInstallBuildpack { let app_dir = &context.app_dir; let package_json = PackageJson::read(app_dir.join("package.json")) .map_err(NpmInstallBuildpackError::PackageJson)?; + let node_build_scripts_metadata = read_node_build_scripts_metadata(&context.buildpack_plan) + .map_err(NpmInstallBuildpackError::NodeBuildScriptsMetadata)?; run_application_checks(app_dir, &warn_later)?; @@ -84,7 +91,12 @@ impl Buildpack for NpmInstallBuildpack { let logger = section.end_section(); let section = logger.section("Running scripts"); - run_build_scripts(&package_json, &env, section.as_ref())?; + run_build_scripts( + &package_json, + &node_build_scripts_metadata, + &env, + section.as_ref(), + )?; let logger = section.end_section(); let section = logger.section("Configuring default processes"); @@ -154,6 +166,7 @@ fn run_npm_install( fn run_build_scripts( package_json: &PackageJson, + node_build_scripts_metadata: &NodeBuildScriptsMetadata, env: &Env, _section_logger: &dyn SectionLogger, ) -> Result<(), NpmInstallBuildpackError> { @@ -162,16 +175,23 @@ fn run_build_scripts( log_step("No build scripts found"); } else { for script in build_scripts { - let mut npm_run = npm::RunScript { env, script }.into_command(); - log_step_stream( - format!("Running {}", fmt::command(npm_run.name())), - |stream| { - npm_run - .stream_output(stream.io(), stream.io()) - .and_then(NamedOutput::nonzero_captured) - .map_err(NpmInstallBuildpackError::BuildScript) - }, - )?; + if let Some(false) = node_build_scripts_metadata.enabled { + log_step(format!( + "Not running {} as it was disabled by a participating buildpack", + fmt::value(script) + )); + } else { + let mut npm_run = npm::RunScript { env, script }.into_command(); + log_step_stream( + format!("Running {}", fmt::command(npm_run.name())), + |stream| { + npm_run + .stream_output(stream.io(), stream.io()) + .and_then(NamedOutput::nonzero_captured) + .map_err(NpmInstallBuildpackError::BuildScript) + }, + )?; + } } }; Ok(()) diff --git a/buildpacks/nodejs-npm-install/tests/integration_test.rs b/buildpacks/nodejs-npm-install/tests/integration_test.rs index de0cc86a..449e6766 100644 --- a/buildpacks/nodejs-npm-install/tests/integration_test.rs +++ b/buildpacks/nodejs-npm-install/tests/integration_test.rs @@ -3,12 +3,14 @@ // Required due to: https://github.com/rust-lang/rust-clippy/issues/11119 #![allow(clippy::unwrap_used)] -use libcnb_test::{assert_contains, assert_not_contains, PackResult}; +use indoc::indoc; +use libcnb::data::buildpack_id; +use libcnb_test::{assert_contains, assert_not_contains, BuildpackReference, PackResult}; use serde_json::json; use std::path::Path; use test_support::{ - add_build_script, add_package_json_dependency, nodejs_integration_test, - nodejs_integration_test_with_config, update_json_file, + add_build_script, add_package_json_dependency, custom_buildpack, integration_test_with_config, + nodejs_integration_test, nodejs_integration_test_with_config, update_json_file, }; #[test] @@ -218,6 +220,55 @@ fn test_native_modules_are_recompiled_even_on_cache_restore() { ); } +#[test] +#[ignore = "integration test"] +fn test_skip_build_scripts_from_buildplan() { + integration_test_with_config( + "./fixtures/npm-project", + |config| { + config.app_dir_preprocessor(|app_dir| { + add_build_script(&app_dir, "heroku-prebuild"); + add_build_script(&app_dir, "build"); + add_build_script(&app_dir, "heroku-postbuild"); + }); + }, + |ctx| { + assert_contains!( + ctx.pack_stdout, + "Not running `heroku-prebuild` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "Not running `build` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "Not running `heroku-postbuild` as it was disabled by a participating buildpack" + ); + }, + &[ + BuildpackReference::WorkspaceBuildpack(buildpack_id!("heroku/nodejs")), + BuildpackReference::Other( + custom_buildpack() + .id("test/skip-build-scripts") + .detect(indoc! { r#" + #!/usr/bin/env bash + + build_plan="$2" + + cat <"$build_plan" + [[requires]] + name = "node_build_scripts" + [requires.metadata] + enabled = false + EOF + "# }) + .call(), + ), + ], + ); +} + fn add_lockfile_entry(app_dir: &Path, package_name: &str, lockfile_entry: serde_json::Value) { update_json_file(&app_dir.join("package-lock.json"), |json| { let dependencies = json["dependencies"].as_object_mut().unwrap(); diff --git a/buildpacks/nodejs-pnpm-install/CHANGELOG.md b/buildpacks/nodejs-pnpm-install/CHANGELOG.md index 5fbf6086..53433bdb 100644 --- a/buildpacks/nodejs-pnpm-install/CHANGELOG.md +++ b/buildpacks/nodejs-pnpm-install/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow configuration of build script behavior through the `node_build_scripts` build plan. ([#928](https://github.com/heroku/buildpacks-nodejs/pull/928)) + ## [3.2.15] - 2024-10-04 - No changes. diff --git a/buildpacks/nodejs-pnpm-install/README.md b/buildpacks/nodejs-pnpm-install/README.md index b7df4c1d..d710d7c0 100644 --- a/buildpacks/nodejs-pnpm-install/README.md +++ b/buildpacks/nodejs-pnpm-install/README.md @@ -52,12 +52,6 @@ This buildpack's `bin/detect` will only pass if a `pnpm-lock.json` exists in the project root. This is done to prevent the buildpack from providing indeterminate and unpredictable dependency trees. -### Build Plan - -This buildpack `requires` `node` (from the [heroku/nodejs-engine](../nodejs-engine) buildpack) -and `pnpm` (from the [heroku/nodejs-corepack](../nodejs-corepack) buildpack). -It also `provides` and `requires` and `node_modules`. - ### Hoist Modes The `hoist = true`, `hoist = false`, `shamefully-hoist = false`, @@ -106,7 +100,51 @@ buildpacks: pack build example-app-image --builder heroku/builder:22 --path /some/example-app ``` +## Build Plan + +### Provides + +| Name | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------| +| `node_modules` | Allows other buildpacks to depend on the Node modules provided by this buildpack. | +| `node_build_scripts` | Allows other buildpacks to depend on the [build script execution](#scripts) behavior provided by this buildpack. | + +### Requires + +| Name | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `node` | To execute `pnpm` a [Node.js][Node.js] runtime is required. It can be provided by the [`heroku/nodejs-engine`][heroku/nodejs-engine] buildpack. | +| `pnpm` | To install node modules, the [pnpm][pnpm] package manager is required. It can be provided by the [`heroku/nodejs-corepack`][heroku/nodejs-corepack] buildpack. | +| `node_modules` | This is not a strict requirement of the buildpack. Requiring `node_modules` ensures that this buildpack can be used even when no other buildpack requires `node_modules`. | +| `node_build_scripts` | This is not a strict requirement of the buildpack. Requiring `node_build_scripts` ensures that this buildpack will perform [build script execution](#scripts) even when no other buildpack requires `node_build_scripts`. | | + +#### Build Plan Metadata Schemas + +##### `node_build_scripts` + +* `enabled` ([boolean][toml_type_boolean], optional) + +###### Example + +```toml +[[requires]] +name = "node_build_scripts" + +[requires.metadata] +enabled = false # this will prevent build scripts from running +``` + ## Additional Info For development, dependencies, contribution, license and other info, please refer to the [root README.md](../../README.md). + +[Node.js]: https://nodejs.org/ + +[pnpm]: https://pnpm.io/ + +[heroku/nodejs-engine]: ../nodejs-engine/README.md + +[heroku/nodejs-corepack]: ../nodejs-corepack/README.md + +[toml_type_boolean]: https://toml.io/en/v1.0.0#boolean diff --git a/buildpacks/nodejs-pnpm-install/src/errors.rs b/buildpacks/nodejs-pnpm-install/src/errors.rs index 568512dc..279cf938 100644 --- a/buildpacks/nodejs-pnpm-install/src/errors.rs +++ b/buildpacks/nodejs-pnpm-install/src/errors.rs @@ -1,5 +1,8 @@ use crate::cmd; use crate::PnpmInstallBuildpackError; +use heroku_nodejs_utils::buildplan::{ + NodeBuildScriptsMetadataError, NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME, +}; use indoc::formatdoc; use libherokubuildpack::log::log_error; @@ -49,7 +52,7 @@ fn on_buildpack_error(bp_err: PnpmInstallBuildpackError) { PnpmInstallBuildpackError::PnpmInstall(err) => { let (context, details) = get_cmd_error_context(err); log_error( - "heroku/nodejs-pnpm pnpm install error", + "pnpm install error", formatdoc! {" There was an error while attempting to install dependencies with pnpm. {context} @@ -61,7 +64,7 @@ fn on_buildpack_error(bp_err: PnpmInstallBuildpackError) { PnpmInstallBuildpackError::PnpmDir(err) => { let (context, details) = get_cmd_error_context(err); log_error( - "heroku/nodejs-pnpm directory error", + "directory error", formatdoc! {" There was an error while attempting to configure a pnpm store to a buildpack layer directory. {context} @@ -73,7 +76,7 @@ fn on_buildpack_error(bp_err: PnpmInstallBuildpackError) { PnpmInstallBuildpackError::PnpmStorePrune(err) => { let (context, details) = get_cmd_error_context(err); log_error( - "heroku/nodejs-pnpm store error", + "store error", formatdoc! {" There was an error while attempting to prune the pnpm content-addressable store. {context} @@ -84,7 +87,7 @@ fn on_buildpack_error(bp_err: PnpmInstallBuildpackError) { } PnpmInstallBuildpackError::VirtualLayer(err) => { log_error( - "heroku/nodejs-pnpm-install virtual store layer error", + "virtual store layer error", formatdoc! {" There was an error while attempting to create the virtual store layer for pnpm's installed dependencies. @@ -93,6 +96,25 @@ fn on_buildpack_error(bp_err: PnpmInstallBuildpackError) { "}, ); } + PnpmInstallBuildpackError::NodeBuildScriptsMetadata(err) => { + let NodeBuildScriptsMetadataError::InvalidEnabledValue(value) = err; + let value_type = value.type_str(); + log_error( + format!("metadata error in {NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME} build plan"), + formatdoc! {" + A participating buildpack has set invalid `[requires.metadata]` for the + build plan named `{NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME}`. + + Expected metadata format: + [requires.metadata] + enabled = + + But was: + [requires.metadata] + enabled = <{value_type}> + "}, + ); + } }; } diff --git a/buildpacks/nodejs-pnpm-install/src/main.rs b/buildpacks/nodejs-pnpm-install/src/main.rs index 9bc9a8eb..f045d96c 100644 --- a/buildpacks/nodejs-pnpm-install/src/main.rs +++ b/buildpacks/nodejs-pnpm-install/src/main.rs @@ -11,6 +11,10 @@ use libherokubuildpack::log::{log_header, log_info}; use crate::configure_pnpm_store_directory::configure_pnpm_store_directory; use crate::configure_pnpm_virtual_store_directory::configure_pnpm_virtual_store_directory; +use heroku_nodejs_utils::buildplan::{ + read_node_build_scripts_metadata, NodeBuildScriptsMetadataError, + NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME, +}; #[cfg(test)] use libcnb_test as _; #[cfg(test)] @@ -40,10 +44,12 @@ impl Buildpack for PnpmInstallBuildpack { DetectResultBuilder::pass() .build_plan( BuildPlanBuilder::new() - .requires("pnpm") .provides("node_modules") - .requires("node_modules") + .provides(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) .requires("node") + .requires("pnpm") + .requires("node_modules") + .requires(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) .build(), ) .build() @@ -55,6 +61,8 @@ impl Buildpack for PnpmInstallBuildpack { let env = Env::from_current(); let pkg_json = PackageJson::read(context.app_dir.join("package.json")) .map_err(PnpmInstallBuildpackError::PackageJson)?; + let node_build_scripts_metadata = read_node_build_scripts_metadata(&context.buildpack_plan) + .map_err(PnpmInstallBuildpackError::NodeBuildScriptsMetadata)?; log_header("Setting up pnpm dependency store"); configure_pnpm_store_directory(&context, &env)?; @@ -77,8 +85,14 @@ impl Buildpack for PnpmInstallBuildpack { log_info("No build scripts found"); } else { for script in scripts { - log_info(format!("Running `{script}` script")); - cmd::pnpm_run(&env, &script).map_err(PnpmInstallBuildpackError::BuildScript)?; + if let Some(false) = node_build_scripts_metadata.enabled { + log_info(format!( + "! Not running `{script}` as it was disabled by a participating buildpack", + )); + } else { + log_info(format!("Running `{script}` script")); + cmd::pnpm_run(&env, &script).map_err(PnpmInstallBuildpackError::BuildScript)?; + } } } @@ -113,6 +127,7 @@ enum PnpmInstallBuildpackError { PnpmInstall(cmd::Error), PnpmStorePrune(cmd::Error), VirtualLayer(std::io::Error), + NodeBuildScriptsMetadata(NodeBuildScriptsMetadataError), } impl From for libcnb::Error { diff --git a/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/package.json b/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/package.json new file mode 100644 index 00000000..859aae65 --- /dev/null +++ b/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/package.json @@ -0,0 +1,9 @@ +{ + "name": "pnpm-9", + "private": true, + "scripts": {}, + "packageManager": "pnpm@9.11.0+sha512.0a203ffaed5a3f63242cd064c8fb5892366c103e328079318f78062f24ea8c9d50bc6a47aa3567cabefd824d170e78fa2745ed1f16b132e16436146b7688f19b", + "dependencies": { + "dotenv": "16.4.5" + } +} diff --git a/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/pnpm-lock.yaml b/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/pnpm-lock.yaml new file mode 100644 index 00000000..a9547fe1 --- /dev/null +++ b/buildpacks/nodejs-pnpm-install/tests/fixtures/pnpm-9/pnpm-lock.yaml @@ -0,0 +1,23 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + dotenv: + specifier: 16.4.5 + version: 16.4.5 + +packages: + + dotenv@16.4.5: + resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} + engines: {node: '>=12'} + +snapshots: + + dotenv@16.4.5: {} diff --git a/buildpacks/nodejs-pnpm-install/tests/integration_test.rs b/buildpacks/nodejs-pnpm-install/tests/integration_test.rs index 86224b79..7db24d56 100644 --- a/buildpacks/nodejs-pnpm-install/tests/integration_test.rs +++ b/buildpacks/nodejs-pnpm-install/tests/integration_test.rs @@ -1,9 +1,13 @@ // Required due to: https://github.com/rust-lang/rust/issues/95513 #![allow(unused_crate_dependencies)] -use indoc::formatdoc; -use libcnb_test::{assert_contains, assert_empty}; -use test_support::{assert_web_response, nodejs_integration_test}; +use indoc::{formatdoc, indoc}; +use libcnb::data::buildpack_id; +use libcnb_test::{assert_contains, assert_empty, BuildpackReference}; +use test_support::{ + add_build_script, assert_web_response, custom_buildpack, integration_test_with_config, + nodejs_integration_test, +}; #[test] #[ignore = "integration test"] @@ -162,3 +166,52 @@ fn test_native_modules_are_recompiled_even_on_cache_restore() { }); }); } + +#[test] +#[ignore = "integration test"] +fn test_skip_build_scripts_from_buildplan() { + integration_test_with_config( + "./fixtures/pnpm-9", + |config| { + config.app_dir_preprocessor(|app_dir| { + add_build_script(&app_dir, "heroku-prebuild"); + add_build_script(&app_dir, "build"); + add_build_script(&app_dir, "heroku-postbuild"); + }); + }, + |ctx| { + assert_contains!( + ctx.pack_stdout, + "! Not running `heroku-prebuild` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "! Not running `build` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "! Not running `heroku-postbuild` as it was disabled by a participating buildpack" + ); + }, + &[ + BuildpackReference::WorkspaceBuildpack(buildpack_id!("heroku/nodejs")), + BuildpackReference::Other( + custom_buildpack() + .id("test/skip-build-scripts") + .detect(indoc! { r#" + #!/usr/bin/env bash + + build_plan="$2" + + cat <"$build_plan" + [[requires]] + name = "node_build_scripts" + [requires.metadata] + enabled = false + EOF + "# }) + .call(), + ), + ], + ); +} diff --git a/buildpacks/nodejs-yarn/CHANGELOG.md b/buildpacks/nodejs-yarn/CHANGELOG.md index 39ddeaa8..bd82ea27 100644 --- a/buildpacks/nodejs-yarn/CHANGELOG.md +++ b/buildpacks/nodejs-yarn/CHANGELOG.md @@ -7,8 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Allow configuration of build script behavior through the `node_build_scripts` build plan. ([#928](https://github.com/heroku/buildpacks-nodejs/pull/928)) - Added Yarn version 4.5.1. - Added Yarn version 3.8.6. + ## [3.2.15] - 2024-10-04 - No changes. diff --git a/buildpacks/nodejs-yarn/Cargo.toml b/buildpacks/nodejs-yarn/Cargo.toml index b446a53d..5de5fe80 100644 --- a/buildpacks/nodejs-yarn/Cargo.toml +++ b/buildpacks/nodejs-yarn/Cargo.toml @@ -16,6 +16,7 @@ thiserror = "1" toml = "0.8" [dev-dependencies] +indoc = "2" libcnb-test = "=0.23.0" test_support.workspace = true ureq = "2" diff --git a/buildpacks/nodejs-yarn/README.md b/buildpacks/nodejs-yarn/README.md index 3ba355c4..944a58a1 100644 --- a/buildpacks/nodejs-yarn/README.md +++ b/buildpacks/nodejs-yarn/README.md @@ -39,11 +39,6 @@ This buildpack's `bin/detect` will only pass if a `yarn.lock` exists in the project root. This is done to prevent the buildpack from providing indeterminate and unpredictable dependency trees. -### Build Plan - -This buildpack `requires` `node` (from the [heroku/nodejs-engine](../nodejs-engine) buildpack). -It also `provides` and `requires` `yarn` and `node_modules`. - ### Environment Variables #### PATH @@ -86,7 +81,7 @@ line. There are two ways to select a different yarn version: Use the `heroku/nodejs-corepack` buildpack to install yarn. It will install yarn according to the `packageManager` key in `package.json`. For example: -```js +```json5 // package.json { "packageManager": "yarn@3.1.2" @@ -95,10 +90,10 @@ yarn according to the `packageManager` key in `package.json`. For example: #### `engines.yarn` -Alternatively, define `engines.yarn` using a semver range in `package.json`. +Alternatively, define `engines.yarn` using a semver range in `package.json`. For example: -```js +```json5 // package.json { "engines": { @@ -117,7 +112,52 @@ command from Cloud Native Buildpacks using both the pack build example-app-image --buildpack heroku/nodejs-engine --buildpack heroku/nodejs-yarn --path /some/example-app ``` +## Build Plan + +### Provides + +| Name | Description | +|----------------------|------------------------------------------------------------------------------------------------------------------| +| `yarn` | Allows other buildpacks that require [Yarn][yarn] tooling to depend on this buildpack. | +| `node_modules` | Allows other buildpacks to depend on the Node modules provided by this buildpack. | +| `node_build_scripts` | Allows other buildpacks to depend on the [build script execution](#scripts) behavior provided by this buildpack. | + +### Requires + +| Name | Description | +|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `node` | To execute `pnpm` a [Node.js][Node.js] runtime is required. It can be provided by the [`heroku/nodejs-engine`][heroku/nodejs-engine] buildpack. | +| `yarn` | To install node modules, the [Yarn][yarn] package manager is required. It can be provided by either the [`heroku/nodejs-corepack`][heroku/nodejs-corepack] buildpack or this one. | +| `node_modules` | This is not a strict requirement of the buildpack. Requiring `node_modules` ensures that this buildpack can be used even when no other buildpack requires `node_modules`. | +| `node_build_scripts` | This is not a strict requirement of the buildpack. Requiring `node_build_scripts` ensures that this buildpack will perform [build script execution](#scripts) even when no other buildpack requires `node_build_scripts`. | | + +#### Build Plan Metadata Schemas + +##### `node_build_scripts` + +* `enabled` ([boolean][toml_type_boolean], optional) + +###### Example + +```toml +[[requires]] +name = "node_build_scripts" + +[requires.metadata] +enabled = false # this will prevent build scripts from running +``` + ## Additional Info For development, dependencies, contribution, license and other info, please refer to the [root README.md](../../README.md). + +[Node.js]: https://nodejs.org/ + +[yarn]: https://yarnpkg.com/ + +[heroku/nodejs-engine]: ../nodejs-engine/README.md + +[heroku/nodejs-corepack]: ../nodejs-corepack/README.md + +[toml_type_boolean]: https://toml.io/en/v1.0.0#boolean diff --git a/buildpacks/nodejs-yarn/src/main.rs b/buildpacks/nodejs-yarn/src/main.rs index 97413ae2..2e7493e0 100644 --- a/buildpacks/nodejs-yarn/src/main.rs +++ b/buildpacks/nodejs-yarn/src/main.rs @@ -16,6 +16,12 @@ use thiserror::Error; use crate::configure_yarn_cache::{configure_yarn_cache, DepsLayerError}; use crate::install_yarn::{install_yarn, CliLayerError}; +use heroku_nodejs_utils::buildplan::{ + read_node_build_scripts_metadata, NodeBuildScriptsMetadataError, + NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME, +}; +#[cfg(test)] +use indoc as _; #[cfg(test)] use libcnb_test as _; #[cfg(test)] @@ -49,10 +55,12 @@ impl Buildpack for YarnBuildpack { .build_plan( BuildPlanBuilder::new() .provides("yarn") - .requires("yarn") .provides("node_modules") - .requires("node_modules") + .provides(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) .requires("node") + .requires("yarn") + .requires("node_modules") + .requires(NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) .build(), ) .build() @@ -64,6 +72,8 @@ impl Buildpack for YarnBuildpack { let mut env = Env::from_current(); let pkg_json = PackageJson::read(context.app_dir.join("package.json")) .map_err(YarnBuildpackError::PackageJson)?; + let node_build_scripts_metadata = read_node_build_scripts_metadata(&context.buildpack_plan) + .map_err(YarnBuildpackError::NodeBuildScriptsMetadata)?; let yarn_version = match cmd::yarn_version(&env) { // Install yarn if it's not present. @@ -133,8 +143,14 @@ impl Buildpack for YarnBuildpack { log_info("No build scripts found"); } else { for script in scripts { - log_info(format!("Running `{script}` script")); - cmd::yarn_run(&env, &script).map_err(YarnBuildpackError::BuildScript)?; + if let Some(false) = node_build_scripts_metadata.enabled { + log_info(format!( + "! Not running `{script}` as it was disabled by a participating buildpack", + )); + } else { + log_info(format!("Running `{script}` script")); + cmd::yarn_run(&env, &script).map_err(YarnBuildpackError::BuildScript)?; + } } } @@ -188,6 +204,9 @@ impl Buildpack for YarnBuildpack { | YarnBuildpackError::YarnDefaultParse(_) => { log_error("Yarn version error", err_string); } + YarnBuildpackError::NodeBuildScriptsMetadata(_) => { + log_error("Yarn buildplan error", err_string); + } } } err => { @@ -223,6 +242,8 @@ enum YarnBuildpackError { YarnVersionResolve(Requirement), #[error("Couldn't parse yarn default version range: {0}")] YarnDefaultParse(VersionError), + #[error("Couldn't parse metadata for the buildplan named {NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME}: {0:?}")] + NodeBuildScriptsMetadata(NodeBuildScriptsMetadataError), } impl From for libcnb::Error { diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.editorconfig b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.editorconfig new file mode 100644 index 00000000..1ed453a3 --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.editorconfig @@ -0,0 +1,10 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +[*.{js,json,yml}] +charset = utf-8 +indent_style = space +indent_size = 2 diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitattributes b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitattributes new file mode 100644 index 00000000..af3ad128 --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitattributes @@ -0,0 +1,4 @@ +/.yarn/** linguist-vendored +/.yarn/releases/* binary +/.yarn/plugins/**/* binary +/.pnp.* binary linguist-generated diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitignore b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitignore new file mode 100644 index 00000000..870eb6a5 --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/.gitignore @@ -0,0 +1,13 @@ +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions + +# Swap the comments on the following lines if you wish to use zero-installs +# In that case, don't forget to run `yarn config set enableGlobalCache false`! +# Documentation here: https://yarnpkg.com/features/caching#zero-installs + +#!.yarn/cache +.pnp.* diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/README.md b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/README.md new file mode 100644 index 00000000..8e777948 --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/README.md @@ -0,0 +1 @@ +# yarn-project diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/package.json b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/package.json new file mode 100644 index 00000000..7e532e27 --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/package.json @@ -0,0 +1,5 @@ +{ + "name": "yarn-project", + "packageManager": "yarn@4.2.2", + "scripts": {} +} diff --git a/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/yarn.lock b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/yarn.lock new file mode 100644 index 00000000..0ecebe7c --- /dev/null +++ b/buildpacks/nodejs-yarn/tests/fixtures/yarn-project/yarn.lock @@ -0,0 +1,12 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"yarn-project@workspace:.": + version: 0.0.0-use.local + resolution: "yarn-project@workspace:." + languageName: unknown + linkType: soft diff --git a/buildpacks/nodejs-yarn/tests/integration_test.rs b/buildpacks/nodejs-yarn/tests/integration_test.rs index 41a4d855..a94e11d3 100644 --- a/buildpacks/nodejs-yarn/tests/integration_test.rs +++ b/buildpacks/nodejs-yarn/tests/integration_test.rs @@ -1,8 +1,13 @@ // Required due to: https://github.com/rust-lang/rust/issues/95513 #![allow(unused_crate_dependencies)] -use libcnb_test::{assert_contains, assert_not_contains}; -use test_support::{assert_web_response, nodejs_integration_test}; +use indoc::indoc; +use libcnb::data::buildpack_id; +use libcnb_test::{assert_contains, assert_not_contains, BuildpackReference}; +use test_support::{ + add_build_script, assert_web_response, custom_buildpack, integration_test_with_config, + nodejs_integration_test, +}; #[test] #[ignore = "integration test"] @@ -160,3 +165,52 @@ fn test_native_modules_are_recompiled_even_on_cache_restore() { }); }); } + +#[test] +#[ignore = "integration test"] +fn test_skip_build_scripts_from_buildplan() { + integration_test_with_config( + "./fixtures/yarn-project", + |config| { + config.app_dir_preprocessor(|app_dir| { + add_build_script(&app_dir, "heroku-prebuild"); + add_build_script(&app_dir, "build"); + add_build_script(&app_dir, "heroku-postbuild"); + }); + }, + |ctx| { + assert_contains!( + ctx.pack_stdout, + "! Not running `heroku-prebuild` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "! Not running `build` as it was disabled by a participating buildpack" + ); + assert_contains!( + ctx.pack_stdout, + "! Not running `heroku-postbuild` as it was disabled by a participating buildpack" + ); + }, + &[ + BuildpackReference::WorkspaceBuildpack(buildpack_id!("heroku/nodejs")), + BuildpackReference::Other( + custom_buildpack() + .id("test/skip-build-scripts") + .detect(indoc! { r#" + #!/usr/bin/env bash + + build_plan="$2" + + cat <"$build_plan" + [[requires]] + name = "node_build_scripts" + [requires.metadata] + enabled = false + EOF + "# }) + .call(), + ), + ], + ); +} diff --git a/common/nodejs-utils/Cargo.toml b/common/nodejs-utils/Cargo.toml index 1cd82ad9..e1478f50 100644 --- a/common/nodejs-utils/Cargo.toml +++ b/common/nodejs-utils/Cargo.toml @@ -13,6 +13,7 @@ commons = { git = "https://github.com/heroku/buildpacks-ruby", branch = "main" } heroku-inventory-utils = { git = "https://github.com/heroku/buildpacks-go/", rev = "2a86fae18332b9bd495eb29422c13ac3fcb2d0dc" } indoc = "2" keep_a_changelog_file = "0.1.0" +libcnb-data = "=0.23.0" node-semver = "2" opentelemetry = "0.24" opentelemetry_sdk = { version = "0.24", features = ["trace"] } diff --git a/common/nodejs-utils/src/buildplan.rs b/common/nodejs-utils/src/buildplan.rs new file mode 100644 index 00000000..553c1c35 --- /dev/null +++ b/common/nodejs-utils/src/buildplan.rs @@ -0,0 +1,133 @@ +use libcnb_data::buildpack_plan::BuildpackPlan; + +#[derive(Debug, Default, PartialEq)] +pub struct NodeBuildScriptsMetadata { + pub enabled: Option, +} + +pub const NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME: &str = "node_build_scripts"; +const NODE_BUILD_SCRIPTS_METADATA_ENABLED_KEY: &str = "enabled"; + +pub fn read_node_build_scripts_metadata( + buildpack_plan: &BuildpackPlan, +) -> Result { + buildpack_plan + .entries + .iter() + .filter(|entry| entry.name == NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME) + .try_fold( + NodeBuildScriptsMetadata::default(), + |mut node_build_hooks_metadata, entry| { + match entry.metadata.get(NODE_BUILD_SCRIPTS_METADATA_ENABLED_KEY) { + Some(toml::Value::Boolean(enabled)) => { + node_build_hooks_metadata.enabled = Some(*enabled); + } + Some(value) => { + Err(NodeBuildScriptsMetadataError::InvalidEnabledValue( + value.clone(), + ))?; + } + None => {} + } + Ok(node_build_hooks_metadata) + }, + ) +} + +#[derive(Debug)] +pub enum NodeBuildScriptsMetadataError { + InvalidEnabledValue(toml::Value), +} + +#[cfg(test)] +mod test { + use super::*; + use libcnb_data::buildpack_plan::{BuildpackPlan, Entry}; + use toml::{toml, Table}; + + #[test] + fn read_node_build_scripts_when_buildpack_plan_contains_no_entries() { + let buildpack_plan = BuildpackPlan { entries: vec![] }; + assert_eq!( + read_node_build_scripts_metadata(&buildpack_plan).unwrap(), + NodeBuildScriptsMetadata::default() + ); + } + + #[test] + fn read_node_build_scripts_when_entry_is_present_with_no_metadata() { + let buildpack_plan = BuildpackPlan { + entries: vec![Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: Table::new(), + }], + }; + assert_eq!( + read_node_build_scripts_metadata(&buildpack_plan).unwrap(), + NodeBuildScriptsMetadata::default() + ); + } + + #[test] + fn read_node_build_scripts_when_entry_is_present_and_metadata_is_declared() { + let buildpack_plan = BuildpackPlan { + entries: vec![Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: toml! { + enabled = false + }, + }], + }; + assert_eq!( + read_node_build_scripts_metadata(&buildpack_plan).unwrap(), + NodeBuildScriptsMetadata { + enabled: Some(false) + } + ); + } + + #[test] + fn read_node_build_scripts_when_multiple_entries_are_present_and_metadata_is_declared() { + let buildpack_plan = BuildpackPlan { + entries: vec![ + Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: toml! { + enabled = false + }, + }, + Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: toml! { + enabled = true + }, + }, + Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: Table::new(), + }, + ], + }; + assert_eq!( + read_node_build_scripts_metadata(&buildpack_plan).unwrap(), + NodeBuildScriptsMetadata { + enabled: Some(true) + } + ); + } + + #[test] + fn read_node_build_scripts_when_entry_contains_invalid_metadata() { + let buildpack_plan = BuildpackPlan { + entries: vec![Entry { + name: NODE_BUILD_SCRIPTS_BUILD_PLAN_NAME.to_string(), + metadata: toml! { + enabled = 0 + }, + }], + }; + match read_node_build_scripts_metadata(&buildpack_plan).unwrap_err() { + NodeBuildScriptsMetadataError::InvalidEnabledValue(_) => {} + } + } +} diff --git a/common/nodejs-utils/src/lib.rs b/common/nodejs-utils/src/lib.rs index 8da8b31e..edad3cd9 100644 --- a/common/nodejs-utils/src/lib.rs +++ b/common/nodejs-utils/src/lib.rs @@ -2,6 +2,7 @@ use keep_a_changelog_file as _; use sha2 as _; pub mod application; +pub mod buildplan; pub mod distribution; pub mod inv; mod nodejs_org; diff --git a/test_support/Cargo.toml b/test_support/Cargo.toml index 596020fd..7fba8ffc 100644 --- a/test_support/Cargo.toml +++ b/test_support/Cargo.toml @@ -7,7 +7,9 @@ edition.workspace = true workspace = true [dependencies] +bon = "2" libcnb = "=0.23.0" libcnb-test = "=0.23.0" serde_json = "1" +tempfile = "3" ureq = "2" diff --git a/test_support/src/lib.rs b/test_support/src/lib.rs index 8d9d28c9..b1dddd08 100644 --- a/test_support/src/lib.rs +++ b/test_support/src/lib.rs @@ -7,9 +7,9 @@ use libcnb_test::{ TestContext, TestRunner, }; use std::net::SocketAddr; -use std::panic; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; +use std::{fs, panic}; const DEFAULT_BUILDER: &str = "heroku/builder:22"; pub const PORT: u16 = 8080; @@ -64,7 +64,7 @@ fn function_integration_test_with_config( ); } -fn integration_test_with_config( +pub fn integration_test_with_config( fixture: &str, with_config: fn(&mut BuildConfig), test_body: fn(TestContext), @@ -215,3 +215,39 @@ pub fn update_json_file(path: &Path, update: impl FnOnce(&mut serde_json::Value) let new_contents = serde_json::to_string(&json).unwrap(); std::fs::write(path, new_contents).unwrap(); } + +#[bon::builder(on(String, into))] +pub fn custom_buildpack(id: &str, detect: Option, build: Option) -> String { + let buildpack_dir = tempfile::tempdir().unwrap().into_path(); + let bin_dir = buildpack_dir.join("bin"); + + fs::create_dir(&bin_dir).unwrap(); + + fs::write( + buildpack_dir.join("buildpack.toml"), + format!( + " +api = \"0.10\" + +[buildpack] +id = \"{id}\" +version = \"0.0.0\" + " + ), + ) + .unwrap(); + + fs::write( + bin_dir.join("detect"), + detect.unwrap_or("#!/usr/bin/env bash".to_string()), + ) + .unwrap(); + + fs::write( + bin_dir.join("build"), + build.unwrap_or("#!/usr/bin/env bash".to_string()), + ) + .unwrap(); + + buildpack_dir.to_string_lossy().to_string() +}