diff --git a/Cargo.lock b/Cargo.lock index 4d13813c..4f921f72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -665,6 +665,17 @@ dependencies = [ "test_support", ] +[[package]] +name = "heroku-pnpm-engine-buildpack" +version = "0.0.0" +dependencies = [ + "commons", + "indoc", + "libcnb", + "libcnb-test", + "test_support", +] + [[package]] name = "hex" version = "0.4.3" diff --git a/Cargo.toml b/Cargo.toml index 0757773d..697074d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "buildpacks/nodejs-function-invoker", "buildpacks/nodejs-npm-engine", "buildpacks/nodejs-npm-install", + "buildpacks/nodejs-pnpm-engine", "buildpacks/nodejs-pnpm-install", "buildpacks/nodejs-yarn", "common/nodejs-utils", diff --git a/buildpacks/nodejs-pnpm-engine/CHANGELOG.md b/buildpacks/nodejs-pnpm-engine/CHANGELOG.md new file mode 100644 index 00000000..a006f47b --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/CHANGELOG.md @@ -0,0 +1,14 @@ +# 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.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial release + +[unreleased]: https://github.com/heroku/buildpacks-nodejs/compare/v2.5.0...HEAD diff --git a/buildpacks/nodejs-pnpm-engine/Cargo.toml b/buildpacks/nodejs-pnpm-engine/Cargo.toml new file mode 100644 index 00000000..e95eb4cb --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "heroku-pnpm-engine-buildpack" +description = "Heroku Node.js pnpm Engine Cloud Native Buildpack" +version.workspace = true +rust-version.workspace = true +edition.workspace = true +publish.workspace = true + +[lints] +workspace = true + +[dependencies] +commons = { git = "https://github.com/heroku/buildpacks-ruby", branch = "main" } +indoc = "2" +libcnb = { version = "=0.17.0", features = ["trace"] } + +[dev-dependencies] +libcnb-test = "=0.17.0" +test_support.workspace = true diff --git a/buildpacks/nodejs-pnpm-engine/README.md b/buildpacks/nodejs-pnpm-engine/README.md new file mode 100644 index 00000000..22a1ff50 --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/README.md @@ -0,0 +1,47 @@ +# Heroku Cloud Native pnpm Engine Buildpack + +[![CI][CI BADGE]][CI LINK] [![Registry][Registry BADGE]][Registry LINK] + +Heroku's official Cloud Native Buildpack for installing a version of `pnpm`. + +> [!IMPORTANT] +> This buildpack is a stub implementation, and does not yet install `pnpm`. +> To install `pnpm` during a build, use [heroku/nodejs-corepack] instead. + +## How it works + +The buildpack will pass detection if: + +- A `pnpm-lock.yaml` file is found at the root of the application source. + +The buildpack will not install `pnpm`, yet. This buildpack will recommend using +`corepack` to install `pnpm` via the [heroku/nodejs-corepack](heroku/nodejs-corepack) +buildpack. + +## Build Plan + +### Requires + +| Name | Description | +|----------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `node` | To install `pnpm` a [Node.js][Node.js] runtime is required. It can be provided by the [`heroku/nodejs-engine`][heroku/nodejs-engine] buildpack. | + +### Provides + +| Name | Description | +|--------|----------------------------------------------------------------------------------------| +| `pnpm` | Allows other buildpacks that require [pnpm][pnpm] tooling to depend on this buildpack. | + +## License + +See [LICENSE](../../LICENSE) file. + +[CI BADGE]: https://github.com/heroku/buildpacks-nodejs/actions/workflows/ci.yml/badge.svg +[CI LINK]: https://github.com/heroku/buildpacks-nodejs/actions/workflows/ci.yml +[Registry BADGE]: https://img.shields.io/badge/dynamic/json?url=https://registry.buildpacks.io/api/v1/buildpacks/heroku/nodejs-pnpm-engine&label=version&query=$.latest.version&color=DF0A6B&logo=data:image/png;base64,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&labelColor=white +[Registry LINK]: https://registry.buildpacks.io/buildpacks/heroku/nodejs-pnpm-engine +[Node.js]: https://nodejs.org/ +[pnpm]: https://pnpm.io/ +[heroku/nodejs-engine]: ../nodejs-engine/README.md +[heroku/nodejs-corepack]: ../nodejs-corepack/README.md + diff --git a/buildpacks/nodejs-pnpm-engine/buildpack.toml b/buildpacks/nodejs-pnpm-engine/buildpack.toml new file mode 100644 index 00000000..0893058c --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/buildpack.toml @@ -0,0 +1,18 @@ +api = "0.9" + +[buildpack] +id = "heroku/nodejs-pnpm-engine" +version = "2.5.0" +name = "Heroku Node.js pnpm Engine" +homepage = "https://github.com/heroku/buildpacks-nodejs" +description = "Heroku's Node.js pnpm Engine buildpack. A component of the 'heroku/nodejs' buildpack." +keywords = ["pnpm", "heroku"] + +[[buildpack.licenses]] +type = "MIT" + +[[stacks]] +id = "*" + +[metadata.release] +image = { repository = "docker.io/heroku/buildpack-nodejs-pnpm-engine" } diff --git a/buildpacks/nodejs-pnpm-engine/src/errors.rs b/buildpacks/nodejs-pnpm-engine/src/errors.rs new file mode 100644 index 00000000..8323eae4 --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/src/errors.rs @@ -0,0 +1,85 @@ +use crate::BUILDPACK_NAME; +use commons::output::build_log::{BuildLog, Logger, StartedLogger}; +use commons::output::fmt; +use commons::output::fmt::DEBUG_INFO; +use indoc::formatdoc; +use std::fmt::Display; +use std::io::stdout; + +#[derive(Debug, Copy, Clone)] +pub(crate) enum PnpmEngineBuildpackError { + CorepackRequired, +} + +pub(crate) fn on_error(error: libcnb::Error) { + 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), + } +} + +fn on_buildpack_error(error: PnpmEngineBuildpackError, logger: Box) { + match error { + PnpmEngineBuildpackError::CorepackRequired => { + print_error_details(logger, &"Corepack Requirement Error") + .announce() + .error(&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 + via the {package_manager} key in {package_json}. + + 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 = fmt::command("corepack enable"), + corepack_use_pnpm = fmt::command("corepack use pnpm@*"), + heroku_nodejs_corepack = fmt::command("heroku/nodejs-corepack"), + package_manager = fmt::value("packageManager"), + pnpm = fmt::value("pnpm"), + pnpm_lockfile = fmt::value("pnpm-lock.yaml"), + package_json = fmt::value("package.json")}); + } + } +} + +fn on_framework_error( + error: &libcnb::Error, + logger: Box, +) { + print_error_details(logger, &error) + .announce() + .error(&formatdoc! {" + {buildpack_name} internal error. + + 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 = fmt::value(BUILDPACK_NAME) }); +} + +fn print_error_details( + logger: Box, + error: &impl Display, +) -> Box { + logger + .section(DEBUG_INFO) + .step(&error.to_string()) + .end_section() +} diff --git a/buildpacks/nodejs-pnpm-engine/src/main.rs b/buildpacks/nodejs-pnpm-engine/src/main.rs new file mode 100644 index 00000000..501dd75e --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/src/main.rs @@ -0,0 +1,55 @@ +mod errors; + +use crate::errors::PnpmEngineBuildpackError; +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 _; +#[cfg(test)] +use test_support as _; + +const BUILDPACK_NAME: &str = "Heroku Node.js pnpm Engine Buildpack"; + +struct PnpmEngineBuildpack; + +impl Buildpack for PnpmEngineBuildpack { + type Platform = GenericPlatform; + type Metadata = GenericMetadata; + type Error = PnpmEngineBuildpackError; + + fn detect(&self, context: DetectContext) -> libcnb::Result { + // pass detect if a `pnpm-lock.yaml` is found + if context.app_dir.join("pnpm-lock.yaml").exists() { + return DetectResultBuilder::pass() + .build_plan( + BuildPlanBuilder::new() + .provides("pnpm") + .requires("node") + .build(), + ) + .build(); + } + DetectResultBuilder::fail().build() + } + + fn build(&self, _context: BuildContext) -> libcnb::Result { + // This buildpack does not install pnpm yet, suggest using + // `heroku/nodejs-corepack` instead. + Err(PnpmEngineBuildpackError::CorepackRequired)? + } + + fn on_error(&self, error: libcnb::Error) { + errors::on_error(error); + } +} + +impl From for libcnb::Error { + fn from(value: PnpmEngineBuildpackError) -> Self { + libcnb::Error::BuildpackError(value) + } +} + +buildpack_main!(PnpmEngineBuildpack); diff --git a/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/.gitigore b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/.gitigore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/.gitigore @@ -0,0 +1 @@ +node_modules/ diff --git a/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/package.json b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/package.json new file mode 100644 index 00000000..51a67603 --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/package.json @@ -0,0 +1,7 @@ +{ + "name": "pnpm-unknown-version", + "version": "1.0.0", + "dependencies": { + "express": "^4.18.2" + } +} diff --git a/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/pnpm-lock.yaml b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/pnpm-lock.yaml new file mode 100644 index 00000000..fb25836e --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/pnpm-lock.yaml @@ -0,0 +1,408 @@ +lockfileVersion: '6.0' + +dependencies: + express: + specifier: ^4.18.2 + version: 4.18.2 + +packages: + + /accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + dev: false + + /array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + dev: false + + /body-parser@1.20.1: + resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.11.0 + raw-body: 2.5.1 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + dev: false + + /call-bind@1.0.2: + resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==} + dependencies: + function-bind: 1.1.1 + get-intrinsic: 1.2.0 + dev: false + + /content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + dev: false + + /cookie-signature@1.0.6: + resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} + dev: false + + /cookie@0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: false + + /debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.0.0 + dev: false + + /depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dev: false + + /destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + dev: false + + /ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + dev: false + + /encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + dev: false + + /escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + dev: false + + /etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + dev: false + + /express@4.18.2: + resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.1 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.5.0 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.2.0 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.1 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.7 + proxy-addr: 2.0.7 + qs: 6.11.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.18.0 + serve-static: 1.15.0 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + dev: false + + /finalhandler@1.2.0: + resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} + engines: {node: '>= 0.8'} + dependencies: + debug: 2.6.9 + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.1 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + dev: false + + /forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + dev: false + + /fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + dev: false + + /function-bind@1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: false + + /get-intrinsic@1.2.0: + resolution: {integrity: sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==} + dependencies: + function-bind: 1.1.1 + has: 1.0.3 + has-symbols: 1.0.3 + dev: false + + /has-symbols@1.0.3: + resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} + engines: {node: '>= 0.4'} + dev: false + + /has@1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: false + + /http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + dev: false + + /iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: false + + /ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + dev: false + + /media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + dev: false + + /merge-descriptors@1.0.1: + resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==} + dev: false + + /methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + dev: false + + /mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + dev: false + + /ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + dev: false + + /object-inspect@1.12.3: + resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + dev: false + + /on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + dependencies: + ee-first: 1.1.1 + dev: false + + /parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + dev: false + + /path-to-regexp@0.1.7: + resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} + dev: false + + /proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + dev: false + + /qs@6.11.0: + resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==} + engines: {node: '>=0.6'} + dependencies: + side-channel: 1.0.4 + dev: false + + /range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + dev: false + + /raw-body@2.5.1: + resolution: {integrity: sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==} + engines: {node: '>= 0.8'} + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + dev: false + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: false + + /safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /send@0.18.0: + resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==} + engines: {node: '>= 0.8.0'} + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 1.0.2 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.0 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.1 + transitivePeerDependencies: + - supports-color + dev: false + + /serve-static@1.15.0: + resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==} + engines: {node: '>= 0.8.0'} + dependencies: + encodeurl: 1.0.2 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.18.0 + transitivePeerDependencies: + - supports-color + dev: false + + /setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + dev: false + + /side-channel@1.0.4: + resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} + dependencies: + call-bind: 1.0.2 + get-intrinsic: 1.2.0 + object-inspect: 1.12.3 + dev: false + + /statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + dev: false + + /toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + dev: false + + /type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + dev: false + + /unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + dev: false + + /utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + dev: false + + /vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + dev: false diff --git a/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/server.js b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/server.js new file mode 100644 index 00000000..f95ef3c6 --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/tests/fixtures/pnpm-unknown-version/server.js @@ -0,0 +1,12 @@ +const express = require("express"); + +const port = process.env['PORT'] || 8080; +const app = express(); + +app.get("/", (_req, res) => { + res.send("Hello from pnpm-unknown-version"); +}); + +app.listen(port, () => { + console.log(`pnpm-unknown-version running on ${port}.`); +}); diff --git a/buildpacks/nodejs-pnpm-engine/tests/integration_test.rs b/buildpacks/nodejs-pnpm-engine/tests/integration_test.rs new file mode 100644 index 00000000..83ae66a5 --- /dev/null +++ b/buildpacks/nodejs-pnpm-engine/tests/integration_test.rs @@ -0,0 +1,38 @@ +// Required due to: https://github.com/rust-lang/rust/issues/95513 +#![allow(unused_crate_dependencies)] + +use indoc::formatdoc; +use libcnb_test::{assert_contains, PackResult}; +use test_support::nodejs_integration_test_with_config; + +#[test] +#[ignore = "integration test"] +fn pnpm_unknown_version() { + nodejs_integration_test_with_config( + "./fixtures/pnpm-unknown-version", + |cfg| { + cfg.expected_pack_result(PackResult::Failure); + }, + |ctx| { + assert_contains!( + ctx.pack_stdout, + &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. + "} + ); + }, + ); +} diff --git a/common/nodejs-utils/src/package_json.rs b/common/nodejs-utils/src/package_json.rs index 27bc5dfe..9cf505b9 100644 --- a/common/nodejs-utils/src/package_json.rs +++ b/common/nodejs-utils/src/package_json.rs @@ -37,8 +37,9 @@ impl PackageJson { #[derive(Deserialize, Debug, Default, Clone)] pub struct Engines { pub node: Option, - pub yarn: Option, pub npm: Option, + pub pnpm: Option, + pub yarn: Option, } #[derive(Deserialize, Debug, Default, Clone)] diff --git a/meta-buildpacks/nodejs/CHANGELOG.md b/meta-buildpacks/nodejs/CHANGELOG.md index e911c489..3a21611a 100644 --- a/meta-buildpacks/nodejs/CHANGELOG.md +++ b/meta-buildpacks/nodejs/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- Add `heroku/nodejs-pnpm-engine`. + ## [2.5.0] - 2023-12-07 ### Changed diff --git a/meta-buildpacks/nodejs/buildpack.toml b/meta-buildpacks/nodejs/buildpack.toml index a8f2548e..774ad27b 100644 --- a/meta-buildpacks/nodejs/buildpack.toml +++ b/meta-buildpacks/nodejs/buildpack.toml @@ -31,6 +31,20 @@ version = "2.5.0" id = "heroku/nodejs-engine" version = "2.5.0" +[[order.group]] +id = "heroku/nodejs-pnpm-engine" +version = "2.5.0" + +[[order.group]] +id = "heroku/nodejs-pnpm-install" +version = "2.5.0" + +[[order]] + +[[order.group]] +id = "heroku/nodejs-engine" +version = "2.5.0" + [[order.group]] id = "heroku/nodejs-corepack" version = "2.5.0" diff --git a/meta-buildpacks/nodejs/package.toml b/meta-buildpacks/nodejs/package.toml index 0554a4d5..3ddfae0b 100644 --- a/meta-buildpacks/nodejs/package.toml +++ b/meta-buildpacks/nodejs/package.toml @@ -10,6 +10,9 @@ uri = "libcnb:heroku/nodejs-npm-engine" [[dependencies]] uri = "libcnb:heroku/nodejs-npm-install" +[[dependencies]] +uri = "libcnb:heroku/nodejs-pnpm-engine" + [[dependencies]] uri = "libcnb:heroku/nodejs-pnpm-install"