diff --git a/.editorconfig b/.editorconfig index cf7c3163c..c3dc0675b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -12,7 +12,7 @@ trim_trailing_whitespace = true # Migrated shell files (kept in sync with the makefile's MIGRATED_FILES list). Tabs are used # so here-documents can be indented: # https://www.gnu.org/software/bash/manual/html_node/Redirections.html#Here-Documents -[{lib/failures.sh,lib/package_managers/*.sh}] +[{lib/failures.sh,lib/package_manager.sh,lib/package_managers/*.sh,lib/utils/*.sh}] binary_next_line = true indent_style = tab shell_variant = bash diff --git a/CHANGELOG.md b/CHANGELOG.md index c86087c0f..82da0f9fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,31 @@ ## [Unreleased] +## [v362] - 2026-08-07 + +- Added Node.js 26.7.0 (linux-amd64) + +## [v361] - 2026-08-03 + +- Added Node.js 26.6.0 (linux-amd64) +- Added Node.js 24.19.0 (linux-amd64) +- Migrated the pnpm lockfile-out-of-sync (`ERR_PNPM_OUTDATED_LOCKFILE`) build error onto the call-site failure-classification framework. ([#1735](https://github.com/heroku/heroku-buildpack-nodejs/pull/1735)) +- Added pnpm to verbose build-summary dependency listing. ([#1737](https://github.com/heroku/heroku-buildpack-nodejs/pull/1737)) + +## [v360] - 2026-07-29 + +- Added Node.js 26.5.1 (linux-amd64) +- Added Node.js 24.18.1 (linux-amd64) +- Added Node.js 22.23.2 (linux-amd64) +- Migrated the Yarn 2+ (Berry) lockfile-out-of-sync (`YN0028`) build error onto the call-site failure-classification framework. ([#1726](https://github.com/heroku/heroku-buildpack-nodejs/pull/1726)) +- Migrated the npm lockfile-out-of-sync (`EUSAGE`) build error onto the call-site failure-classification framework. ([#1727](https://github.com/heroku/heroku-buildpack-nodejs/pull/1727)) + +## [v359] - 2026-07-27 + +- Removed the unused `lib/features.sh` A/B rollout module. ([#1719](https://github.com/heroku/heroku-buildpack-nodejs/pull/1719)) +- Migrated the npm peer-dependency-conflict (`ERESOLVE`) build error onto the call-site failure-classification framework. ([#1720](https://github.com/heroku/heroku-buildpack-nodejs/pull/1720)) +- Migrated the Yarn 1.x outdated-yarn-lockfile build error onto the call-site failure-classification framework. ([#1721](https://github.com/heroku/heroku-buildpack-nodejs/pull/1721)) + ## [v358] - 2026-07-09 - Added Node.js 26.5.0 (linux-amd64) @@ -169,7 +194,7 @@ ## [v328] - 2026-02-02 -- Support pnpm workspace pruning. ([#1535](https://github.com/heroku/heroku-buildpack-nodejs/pull/1535)) +- Support pnpm workspace pruning. ([#1535](https://github.com/heroku/heroku-buildpack-nodejs/pull/1535)) - Fix pnpm workspace detection. ([#1528](https://github.com/heroku/heroku-buildpack-nodejs/pull/1528)) - Fixed issue with parsing pnpm version from the `packageManager` field when an integrity check is present. ([#1540](https://github.com/heroku/heroku-buildpack-nodejs/pull/1540)) @@ -207,7 +232,7 @@ ## [v320] - 2025-12-03 -- Updated default Node.js version to 24.x. ([#1510](https://github.com/heroku/heroku-buildpack-nodejs/pull/1510)) +- Updated default Node.js version to 24.x. ([#1510](https://github.com/heroku/heroku-buildpack-nodejs/pull/1510)) ## [v319] - 2025-11-26 @@ -281,7 +306,7 @@ - Added Node.js 24.6.0 (linux-amd64) - Fix escaping for buildpack metrics. ([#1451](https://github.com/heroku/heroku-buildpack-nodejs/pull/1451)) -- Remove calls to unsupported metrics format (i.e.; `mcount`, `mtime`, `mmeasure`) ([#1454](https://github.com/heroku/heroku-buildpack-nodejs/pull/1454)) +- Remove calls to unsupported metrics format (i.e.; `mcount`, `mtime`, `mmeasure`) ([#1454](https://github.com/heroku/heroku-buildpack-nodejs/pull/1454)) ## [v303] - 2025-08-05 @@ -1388,7 +1413,11 @@ Accepts `cacheDirectories` array in package.json to override default `node_modul - Documented at https://devcenter.heroku.com/articles/nodejs-support#cache-behavior -[unreleased]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v358...main +[unreleased]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v362...main +[v362]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v361...v362 +[v361]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v360...v361 +[v360]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v359...v360 +[v359]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v358...v359 [v358]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v357...v358 [v357]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v356...v357 [v356]: https://github.com/heroku/heroku-buildpack-nodejs/compare/v355...v356 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7fbdd3762..f0dd5d59f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,42 +2,77 @@ Thanks for making a contribution to the Heroku Node.js Buildpack! -*Note: If you are looking to open an issue or make a contribution to the Heroku Node.js Cloud Native Buildpacks (this includes builds made with `pack`), go here: https://github.com/heroku/nodejs-engine-buildpack* +*Note: If you are looking to open an issue or make a contribution to the Heroku Node.js Cloud Native Buildpacks (this includes builds made with `pack`), go here: https://github.com/heroku/buildpacks-nodejs* -## Setup +## Prerequisites -### Install Docker (optional) +For local development, you may need the following tools: -Before you get started, you may want to [install Docker](https://docs.docker.com/engine/install/). Docker will be needed to run the test suite. +- [Docker](https://docs.docker.com/engine/install/) (required to run the test suite) +- [Rust](https://www.rust-lang.org/tools/install) (for building the version resolver in `resolve-version/`) +- The `x86_64-unknown-linux-musl` cross-compilation target and linker (used by `make build-resolvers`) -If you don't install Docker, you have the option of waiting until your pull request open to run tests. [The Travis tests can be found here.](https://travis-ci.com/github/heroku/heroku-buildpack-nodejs/pull_requests) +## Clone the repo -### Clone Repo - -First, you will want to fork and clone the repository. +Fork and clone the repository: ```sh git clone git@github.com:your-username/heroku-buildpack-nodejs.git ``` -Once you have this on your local machine, you're ready to start making changes. +## Deploying an app with a fork or branch + +Push your changes to your fork, then create a new Heroku app to test it, or configure an existing app to use your buildpack: + +```sh +# Create a new Heroku app that uses your buildpack +heroku create --buildpack + +# Configure an existing Heroku app to use your buildpack +heroku buildpacks:set + +# You can also use a git branch! +heroku buildpacks:set #your-branch +``` ## Testing -### Writing Tests +The tests use [Docker](https://www.docker.com/) to simulate Heroku's stacks, and run via the vendored [shunit2](https://github.com/kward/shunit2) test framework. + +For any change you make, write a unit test in `test/run` that would break if you removed the code you've just worked on. Add a fixture (`test/fixtures/*`) if you need an additional sample app to test with. + +Run the full test suite: -There are unit tests that are run with `shunit`. For any change you make, write a unit test in `test/run` that would break if you removed the code that you've just worked on. Add a fixture (`test/fixtures/*`) if you need an additional sample app to test with. [More info about testing here.](https://github.com/heroku/heroku-buildpack-nodejs#tests) +```sh +make test +``` -### Running Tests +Test a single stack (usually adequate): -To run the tests, run `make test`. You will need Docker installed. This will start a test run of all Heroku stack images that will run serially. If you want to test one stack image (which is usually adequate), run `make heroku-24-build` (or whatever stack image you'd like to test). +```sh +make heroku-22-build +make heroku-24-build +make heroku-26-build +``` + +Run just the unit tests: + +```sh +make unit +``` + +Lint and check formatting of the shell scripts: + +```sh +make lint +``` ## Opening a Pull Request -After the work is completed, please open a pull request. If it links to an Issue, please make sure to include in the Description: +After the work is completed, please open a pull request. If it links to an Issue, include it in the description: ```md Fixes: https://github.com/heroku/heroku-buildpack-nodejs/issues/xxx ``` -In the Description, also add an explanation of the work that's done and the tests that are included. +Also add an explanation of the work done and the tests included. diff --git a/Gemfile.lock b/Gemfile.lock index ab0af368b..e3273c722 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,7 +4,7 @@ GEM base64 (0.3.0) diff-lcs (1.6.2) erubis (2.7.0) - excon (1.2.8) + excon (1.5.0) logger heroics (0.1.3) base64 diff --git a/README.md b/README.md index 92aea1cd5..e241cdafd 100644 --- a/README.md +++ b/README.md @@ -1,258 +1,43 @@ Buildpack for Node.js, io.js And Meteor ======================================= -![nodejs](https://raw.githubusercontent.com/heroku/buildpacks/refs/heads/main/assets/images/buildpack-banner-node-js.png) +This is the official [Scalingo buildpack](https://doc.scalingo.com/platform/deployment/buildpacks/intro) for Node.js apps. -This buildpack has been developped to deploy any node.js application -or an application based on the Meteor Framework (0.9+). +## Getting Started -It is based on the original nodejs buildpack and has been updated to -get an out of the box experience for Meteor. +See the [Getting Started with Node.js on Scalingo](https://doc.scalingo.com/languages/nodejs/tutorial) tutorial. -## How it Works +## Application Requirements -Apps are built via one of four paths: +A `package.json` file must be present in the root (top-level) directory of your app's source code. -1. A regular `npm install` (first build; default scenario) -2. Copy existing `node_modules` from cache, then `npm prune`, then `npm install` (subsequent builds) -3. Skip dependencies (if package.json doesn't exist but server.js does) -4. Skip cache, run `npm rebuild` before `npm install` (`node_modules` are checked into source control) +The buildpack supports the npm, Yarn, and pnpm package managers, selected by the lockfile present in your app: +- `package-lock.json` (npm) +- `yarn.lock` (Yarn) +- `pnpm-lock.yaml` (pnpm) -You should only use #3 (omitting package.json) for quick tests or experiments. -You should never use #4 - it's included for backwards-compatibility and will generate warnings. +If no lockfile is found, npm is used. A lockfile is strongly recommended for reproducible builds. -**Adding `node_modules` directory in git repository is an antipattern.** +## Configuration -### Specify Node.js/NPM version +### Node.js Version -Node.js and NPM version are read from the `engines` section of the `package.json` file: https://doc.scalingo.com/languages/nodejs/start#specifying-a-nodejs-version +Specify the Node.js version for your app with the `engines.node` field in `package.json`: -### Common - -- Allows any recent version of node to be used, including [pre-release versions](https://semver.scalingo.com/node.json). -- Discourages use of dangerous semver ranges like `*` and `>0.10`. -- Uses the version of `npm` that comes bundled with `node`. -- Puts `node` and `npm` on the `PATH` so they can be executed with [scalingo run](https://doc.scalingo.com/app/tasks). - -### Caching - -- Caches the `node_modules` directory across builds for fast deploys. -- Meteor specific: we cache the meteor bundle to avoid spending time downloading it at each deployment -- Doesn't use the cache if `node_modules` is checked into version control. -- Runs `npm rebuild` if `node_modules` is checked into version control. -- Always runs `npm install` to ensure [npm script hooks](https://docs.npmjs.com/cli/v8/using-npm/scripts) are executed. -- Always runs `npm prune` after restoring cached modules to ensure cleanup of unused dependencies. - -For more technical details, see the [compile script](https://github.com/Scalingo/nodejs-buildpack/blob/master/bin/compile). - -## Documentation - -For more information about using Node.js and buildpacks on Scalingo, see these Documentation pages: - -- [Scalingo Node.js Support](https://doc.scalingo.com/languages/nodejs/start) -- [Scalingo Meteor Support](https://doc.scalingo.com/languages/meteorjs/start) -- [Buildpacks](https://doc.scalingo.com/buildpacks) -- [Getting Started with Node.js on Scalingo](https://doc.scalingo.com/languages/nodejs/tutorial) -- [Getting Started with Meteor on Scalingo](https://doc.scalingo.com/languages/meteorjs/tutorial) - -## Debug build - -### Meteor - -By default, Meteor minify all your assets to stand in one single javascript file, -if you want to make a 'debug' build (assets unminified), please defined the following -environment variable: - -```shell -scalingo -a app-name env-set METEOR_DEBUG_BUILD=true -``` - -## Options for Meteor - -If you're using the meteor framework, you just need to commit the `.meteor` -directory at the root of your Meteor project, we'll detect and handle it to -make your application work, out of the box. - -If you are using Meteor ≥ 1.3, the flag `--server-only` will be used automatically -to build your application for Meteor mobile integration. - -### Memory in the build process - -Meteor build is handled by a Node.JS process which has a default limit: -it can't use more than ~3.7GB of RAM. It can be too little for large Meteor -applications and the build process will fail with the following error: -`Allocation failed`. - -The buildpack is fixing this issue by adding to the environment variable -`TOOL_NODE_FLAGS`, the flag `--max-old-space-size=8192` (except if this -precise flag is already defined). - -To update the value `8192`, the variable `BUILD_MAX_MEMORY` can be overriden. - -### Starting flags for `node` process - -For some reasons, you may want to use custom flags to run your application. -The following environment variable let you customize this. - -Example: (when meteor need to use 4GB of RAM) - -```shell -scalingo -a app-name env-set NODE_BOOT_FLAGS="--max-old-space-size=4096" -``` - -## Options for Node - -### Enable or disable node_modules caching - -For a 'clean' build without using any cached node modules: - -```shell -scalingo env-set NODE_MODULES_CACHE=false -git commit -am 'rebuild' --allow-empty -git push scalingo master -scalingo env-unset NODE_MODULES_CACHE -``` - -Caching node_modules between builds dramatically speeds up build times. -However, `npm install` doesn't automatically update already-installed modules -as long as they fall within acceptable semver ranges, -which can lead to outdated modules. - -Default: `NODE_MODULES_CACHE` defaults to true - -### Enable or disable devDependencies installation - -During local development, `npm install` installs all dependencies -and all devDependencies (test frameworks, build tools, etc). -This is usually something you want to avoid in production, so -npm has a 'production' config that can be set through the environment: - -To install *dependencies only:* - -```shell -scalingo -a app-name env-set NPM_CONFIG_PRODUCTION=true -``` - -To install *dependencies and devDependencies:* - -```shell -scalingo -a app-name env-set NPM_CONFIG_PRODUCTION=false -``` - -Default: `NPM_CONFIG_PRODUCTION` defaults to false on Scalingo - -### Configure npm with .npmrc - -Sometimes, a project needs custom npm behavior to set up proxies, -use a different registry, etc. For such behavior, -just include an `.npmrc` file in the root of your project: - -``` -# .npmrc -registry = 'https://custom-registry.com/' -``` - -### Reasonable defaults for concurrency - -This buildpack adds two environment variables: `WEB_MEMORY` and `WEB_CONCURRENCY`. -You can set either of them, but if unset the buildpack will fill them with reasonable defaults. - -- `WEB_MEMORY`: expected memory use by each node process (in MB, default: 512) -- `WEB_CONCURRENCY`: recommended number of processes to Cluster based on the current environment - -Clustering is not done automatically; concurrency should be part of the app, -usually via a library like [throng](https://github.com/hunterloftis/throng). -Apps without any clustering mechanism will remain unaffected by these variables. - -This behavior allows your app to automatically take advantage of larger containers. -The default settings will cluster -1 process on a S and M containers, 2 processes on L containers and 4 on XL containers. - -For example, when your app starts: - -```shell -app[web-1]: Detected 1024 MB available memory, 512 MB limit per process (WEB_MEMORY) -app[web-1]: Recommending WEB_CONCURRENCY=2 -app[web-1]: -app[web-1]: > example-concurrency@1.0.0 start /app -app[web-1]: > node server.js -app[web-1]: Listening on 51118 -app[web-1]: Listening on 51118 -``` - -### Chain Node with multiple buildpacks - -This buildpack automatically exports node, npm, and any node_modules binaries -into the `$PATH` for easy use in subsequent buildpacks. - -## Feedback or want to report an issue - -- [GitHub issues](https://github.com/Scalingo/nodejs-buildpack/issues) - -## Hacking - -To make changes to this buildpack, fork it on Github. Push up changes to your fork, then create a new Scalingo app to test it, or configure an existing app to use your buildpack: - -```shell -# Configure an existing Scalingo app to use your buildpack -scalingo -a app-name env-set BUILDPACK_URL= - -# You can also use a git branch! -scalingo -a app-name env-set BUILDPACK_URL=#your-branch -``` - -## Tests - -The buildpack tests use [Docker](https://www.docker.com/) to simulate -Scalingo environment. - -To run the test suite: - -```shell -make test -``` - -Or to just test a specific stack: - -``` -make heroku-22-build -make heroku-24-build -make heroku-26-build +```json +{ + "engines": { + "node": "24.x" + } +} ``` -The tests are run via the vendored -[shunit2](https://github.com/kward/shunit2) -test framework. - -### Debugging - -To display the logged build outputs to assist with debugging, use the "echo" and "cat" commands. For example: - -```sh -test() { - local log_file var - - var="testtest" - log_file=$(mktemp) - echo "this is the log file" > "$log_file" - echo "test log file" >> "$log_file" +We recommend using a major version range (like `24.x`) rather than pinning an exact version, so your app automatically receives Node.js security and bug-fix updates. - # use `echo` and `cat` for printing variables and reading files respectively - echo $var - cat $log_file +If you don't specify a version, the buildpack uses the current recommended LTS release. - # some cases when debugging is necessary - assertEquals "$var" "testtest" - assertFileContains "test log file" "$log_file" -} -``` - -Running the test above would produce: +For the list of supported Node.js versions, see [our documentation](https://doc.scalingo.com/languages/nodejs/start#availability). -```log -testtest -this is the log file -test log file -``` +## Documentation -The test output writes to `$STD_OUT`, so you can use `cat $STD_OUT` to read output. +For more information about using Node.js on Scalingo, see [our documentation](https://doc.scalingo.com/languages/nodejs/start). diff --git a/bin/compile b/bin/compile index 792a099f3..7c460fcc9 100755 --- a/bin/compile +++ b/bin/compile @@ -23,8 +23,6 @@ BP_DIR=$(cd "$(dirname "${0:-}")"; cd ..; pwd) ### Load dependencies -# shellcheck source=lib/vendor/stdlib_v7.sh -source "$BP_DIR/lib/vendor/stdlib_v7.sh" # shellcheck source=lib/output.sh source "$BP_DIR/lib/output.sh" # shellcheck source=lib/monitor.sh @@ -37,32 +35,24 @@ source "$BP_DIR/lib/failures.sh" source "$BP_DIR/lib/package_managers/npm.sh" # shellcheck source=lib/package_managers/pnpm.sh source "$BP_DIR/lib/package_managers/pnpm.sh" +# shellcheck source=lib/package_managers/yarn.sh +source "$BP_DIR/lib/package_managers/yarn.sh" # shellcheck source=lib/_failures.sh source "$BP_DIR/lib/_failures.sh" # shellcheck source=lib/runtimes/nodejs.sh source "$BP_DIR/lib/runtimes/nodejs.sh" -# shellcheck source=lib/binaries.sh -source "$BP_DIR/lib/binaries.sh" +# shellcheck source=lib/utils/command.sh +source "$BP_DIR/lib/utils/command.sh" # shellcheck source=lib/json.sh source "$BP_DIR/lib/json.sh" # shellcheck source=lib/yaml.sh source "$BP_DIR/lib/yaml.sh" # shellcheck source=lib/cache.sh source "$BP_DIR/lib/cache.sh" -# shellcheck source=lib/dependencies.sh -source "$BP_DIR/lib/dependencies.sh" -# shellcheck source=lib/plugin.sh -source "$BP_DIR/lib/plugin.sh" -# shellcheck source=lib/uuid.sh -source "$BP_DIR/lib/uuid.sh" -# shellcheck source=lib/kvstore.sh -source "$BP_DIR/lib/kvstore.sh" -# shellcheck source=lib/features.sh -source "$BP_DIR/lib/features.sh" +# shellcheck source=lib/package_manager.sh +source "$BP_DIR/lib/package_manager.sh" # shellcheck source=lib/builddata.sh source "$BP_DIR/lib/builddata.sh" -# shellcheck source=lib/yarn-2.sh -source "$BP_DIR/lib/yarn-2.sh" # Load meteor framework related functions source $BP_DIR/bin/meteor.sh if test -n "$meteor_version" ; then @@ -87,8 +77,7 @@ export FAILURE_EMITTED_MARKER="${LOG_FILE}.emitted" build_start_time=$(build_data::current_unix_realtime) -### Initialize metrics and features -features_init "nodejs" "$BUILD_DIR" "$CACHE_DIR" "$BP_DIR/features" +### Initialize metrics build_data::setup ### Save build info @@ -106,13 +95,6 @@ for script in scalingo-prebuild scalingo-postbuild build scalingo-cleanup start fi done -# add any active features to the metadata set prefixed with the key "feature_" -features_list | while read -r key; do - if [[ -n $key ]]; then - build_data::set_raw "feature_$key" "$(features_get "$key")" - fi -done - ### Handle errors handle_failure() { @@ -124,7 +106,6 @@ handle_failure() { header "Build failed" fail_using_yarn2_with_yarn_production_environment_variable_set "$LOG_FILE" fail_yarn_outdated "$LOG_FILE" - fail_yarn_lockfile_outdated "$LOG_FILE" fail_yarn_install "$LOG_FILE" "$BUILD_DIR" log_other_failures "$LOG_FILE" warn_untracked_dependencies "$LOG_FILE" @@ -141,7 +122,7 @@ trap 'handle_failure' ERR [ -f "$BUILD_DIR/yarn.lock" ] && YARN=true || YARN=false [ -f "$BUILD_DIR/package-lock.json" ] && NPM_LOCK=true || NPM_LOCK=false [ -f "$BUILD_DIR/pnpm-lock.yaml" ] && PNPM=true || PNPM=false -YARN_2=$(detect_yarn_2 "$YARN" "$BUILD_DIR") +YARN_2=$(package_managers::yarn::detect_berry "$YARN" "$BUILD_DIR") ### Failures that should be caught immediately @@ -188,19 +169,19 @@ if [[ "$YARN_2" == "true" ]]; then # runtime; the cache must live in /app (not /tmp) so that dependencies are # persisted. When using zero-install, /app/.yarn/cache will include all # dependencies which yarn should reuse. - if use_yarn_app_cache "$BUILD_DIR"; then + if package_managers::yarn::berry_use_app_cache "$BUILD_DIR"; then YARN_CACHE_FOLDER=${YARN_CACHE_FOLDER:-"$BUILD_DIR/.yarn/cache"} fi # When in zero-install mode, use --immutable-cache to verify that the cache # is pristine. Cache mutation is expected in nonzero installs. - if has_yarn_cache "$BUILD_DIR"; then + if package_managers::yarn::berry_has_cache "$BUILD_DIR"; then YARN_ENABLE_IMMUTABLE_CACHE=${YARN_ENABLE_IMMUTABLE_CACHE:-true} YARN_ZERO_INSTALL=true fi ### Configure Yarn 2+ - YARN_MAJOR_VERSION=$(get_yarn_major_version "$BUILD_DIR") + YARN_MAJOR_VERSION=$(package_managers::yarn::get_major_version "$BUILD_DIR") # if we're installing yarn via packageManager or engines.yarn we don't need to fail # on the yarn path configuration or vendored script @@ -212,13 +193,13 @@ if [[ "$YARN_2" == "true" ]]; then 1|"") # This ensures we check for a vendored Yarn if we're using Yarn 1 to bootstrap fail_missing_yarnrc_yml "$BUILD_DIR" - VENDOR_PATH=$(get_yarn_path "$BUILD_DIR") + VENDOR_PATH=$(package_managers::yarn::berry_get_path "$BUILD_DIR") fail_missing_yarn_path "$BUILD_DIR" "$VENDOR_PATH" ;; *) # no need to require a vendored yarnPath (though one may still be configured) if [[ -f "$BUILD_DIR/.yarnrc.yml" ]]; then - VENDOR_PATH=$(get_yarn_path "$BUILD_DIR") + VENDOR_PATH=$(package_managers::yarn::berry_get_path "$BUILD_DIR") fi ;; esac @@ -304,10 +285,10 @@ install_bins() { EOF fi - if has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then + if package_managers::yarn::berry_has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then showed_warning=true local release_script - release_script="$(get_yarn_path "$BUILD_DIR")" + release_script="$(package_managers::yarn::berry_get_path "$BUILD_DIR")" output::warning <<-EOF Yarn release script may conflict with "packageManager" @@ -338,11 +319,11 @@ install_bins() { build_data::set_string "build_step" "install-npm" package_managers::npm::install_binary "$npm_engine" "$BUILD_DIR/.scalingo/node" "$NPM_LOCK" - if ! has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then + if ! package_managers::yarn::berry_has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then build_data::set_string "build_step" "install-yarn" yarn_version="$(echo "$package_manager" | cut -d '@' -f 2 | cut -d "#" -f 1)" install_start=$(build_data::current_unix_realtime) - install_yarn "$BUILD_DIR/.scalingo/yarn" "$yarn_version" + package_managers::yarn::install_binary "$BUILD_DIR/.scalingo/yarn" "$yarn_version" build_data::set_duration "install_yarn_binary_time" "$install_start" else # Download yarn if there is a yarn.lock file or if the user @@ -351,7 +332,7 @@ install_bins() { if $YARN || [ -n "$yarn_engine" ]; then build_data::set_string "build_step" "install-yarn" install_start=$(build_data::current_unix_realtime) - install_yarn "$BUILD_DIR/.scalingo/yarn" "$yarn_engine" + package_managers::yarn::install_binary "$BUILD_DIR/.scalingo/yarn" "$yarn_engine" build_data::set_duration "install_yarn_binary_time" "$install_start" fi fi @@ -371,7 +352,7 @@ install_bins() { fi install_start=$(build_data::current_unix_realtime) - install_pnpm "$pnpm_version" + package_managers::pnpm::install_binary "$pnpm_version" build_data::set_duration "install_pnpm_binary_time" "$install_start" if [ "$using_default_pnpm" == true ]; then output::warning <<-EOF @@ -460,7 +441,7 @@ if $PNPM; then fi if [[ -z "$USE_NPM_INSTALL" ]]; then - if [[ "$(should_use_npm_ci "$BUILD_DIR")" == "true" ]]; then + if [[ "$(package_managers::npm::should_use_npm_ci "$BUILD_DIR")" == "true" ]]; then USE_NPM_INSTALL=false else USE_NPM_INSTALL=true @@ -527,25 +508,25 @@ build_dependencies() { cache_status="$(get_cache_status "$CACHE_DIR")" start=$(build_data::current_unix_realtime) if [[ "$YARN_2" == "true" ]]; then - yarn_2_install "$BUILD_DIR" + package_managers::yarn::yarn2_install_dependencies "$BUILD_DIR" elif $YARN; then - yarn_node_modules "$BUILD_DIR" + package_managers::yarn::install_dependencies "$BUILD_DIR" elif $PNPM; then package_managers::pnpm::install_dependencies "$BUILD_DIR" "$CACHE_DIR" elif $PREBUILD; then echo "Prebuild detected (node_modules already exists)" - npm_rebuild "$BUILD_DIR" + package_managers::npm::rebuild_dependencies "$BUILD_DIR" else package_managers::npm::install_dependencies "$BUILD_DIR" fi build_data::set_string "build_step" "build-script" header "Build" - run_build_script "$BUILD_DIR" + package_manager::run_build_script "$BUILD_DIR" } build_data::set_string "build_step" "prebuild-script" -run_prebuild_script "$BUILD_DIR" | output "$LOG_FILE" +package_manager::run_prebuild_script "$BUILD_DIR" | output "$LOG_FILE" header "Installing dependencies" | output "$LOG_FILE" build_data::set_string "build_step" "install-dependencies" @@ -574,11 +555,11 @@ cache_build() { prune_devdependencies() { if $YARN || $YARN_2; then - yarn_prune_devdependencies "$BUILD_DIR" "$YARN_CACHE_FOLDER" "$BP_DIR" + package_managers::yarn::prune_devdependencies "$BUILD_DIR" "$BP_DIR" elif $PNPM; then - pnpm_prune_devdependencies "$BUILD_DIR" + package_managers::pnpm::prune_devdependencies "$BUILD_DIR" else - npm_prune_devdependencies "$BUILD_DIR" + package_managers::npm::prune_devdependencies "$BUILD_DIR" fi } @@ -609,16 +590,17 @@ if test -n "$meteor_version"; then build_meteor_app $BUILD_DIR $CACHE_DIR fi -run_cleanup_script "$BUILD_DIR" | output "$LOG_FILE" +build_data::set_string "build_step" "cleanup-script" +package_manager::run_cleanup_script "$BUILD_DIR" | output "$LOG_FILE" summarize_build() { if $NODE_VERBOSE; then - list_dependencies "$BUILD_DIR" + package_manager::list_dependencies "$BUILD_DIR" fi } build_data::set_string "build_step" "install-metrics-plugin" -install_plugin "$BP_DIR" "$BUILD_DIR" +runtimes::nodejs::install_metrics_plugin "$BP_DIR" "$BUILD_DIR" build_data::set_string "build_step" "summarize" summarize_build | output "$LOG_FILE" diff --git a/features b/features deleted file mode 100644 index e69de29bb..000000000 diff --git a/inventory/node.toml b/inventory/node.toml index 4b2a3c03b..9b3acd4a3 100644 --- a/inventory/node.toml +++ b/inventory/node.toml @@ -1,3 +1,24 @@ +[[artifacts]] +version = "26.7.0" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v26.7.0/node-v26.7.0-linux-x64.tar.gz" +checksum = "sha256:bd6b6c31e377bad9ad579bed72e5bc11f4c879ac9452ad51d30e646ea3d828df" + +[[artifacts]] +version = "26.6.0" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v26.6.0/node-v26.6.0-linux-x64.tar.gz" +checksum = "sha256:714f352a7539c5ce09af15e440cbaadbe935bb23ba23fcee66b066d00a063822" + +[[artifacts]] +version = "26.5.1" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v26.5.1/node-v26.5.1-linux-x64.tar.gz" +checksum = "sha256:2b07f09c218d473a26442bff5a90151f53f7b7c0a23bad244eda2c26303a2ba7" + [[artifacts]] version = "26.5.0" os = "linux" @@ -145,6 +166,20 @@ arch = "amd64" url = "https://nodejs.org/download/release/v25.0.0/node-v25.0.0-linux-x64.tar.gz" checksum = "sha256:28dd46a6733192647d7c8267343f5a3f1c616f773c448e2c0d2539ae70724b40" +[[artifacts]] +version = "24.19.0" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v24.19.0/node-v24.19.0-linux-x64.tar.gz" +checksum = "sha256:f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4" + +[[artifacts]] +version = "24.18.1" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v24.18.1/node-v24.18.1-linux-x64.tar.gz" +checksum = "sha256:9f5eb6ac21845a66c493c91a253b1da32fd684e89e9b7202d4936982336be4ca" + [[artifacts]] version = "24.18.0" os = "linux" @@ -418,6 +453,13 @@ arch = "amd64" url = "https://nodejs.org/download/release/v23.0.0/node-v23.0.0-linux-x64.tar.gz" checksum = "sha256:702cbc710fcf1102cef1aced74443fee34eff8df4827de30ec970d377ce31d9e" +[[artifacts]] +version = "22.23.2" +os = "linux" +arch = "amd64" +url = "https://nodejs.org/download/release/v22.23.2/node-v22.23.2-linux-x64.tar.gz" +checksum = "sha256:b294a556e639d64338823920e5866c21c02741742d2e1529ee1a225c1ec9252a" + [[artifacts]] version = "22.23.1" os = "linux" diff --git a/lib/_failures.sh b/lib/_failures.sh index d28c72326..1c0545d22 100644 --- a/lib/_failures.sh +++ b/lib/_failures.sh @@ -133,7 +133,7 @@ multiple_lockfiles_fix_steps() { echo " If you use $keep:" echo " \$ git rm ${remove[*]}" echo " \$ git commit -m \"Remove unused lockfiles\"" - echo " \$ git push heroku main" + echo " \$ git push scalingo main" echo "" done } @@ -233,29 +233,6 @@ fail_yarn_outdated() { fi } -fail_yarn_lockfile_outdated() { - local log_file="$1" - if grep -qi 'Your lockfile needs to be updated' "$log_file"; then - build_data::set_string "failure" "outdated-yarn-lockfile" - echo "" - warn "Outdated Yarn lockfile - - Your application contains a Yarn lockfile (yarn.lock) which does not - match the dependencies in package.json. This can happen if you use npm - to install or update a dependency instead of Yarn. - - Please run the following command in your application directory and check - in the new yarn.lock file: - - $ yarn install - $ git add yarn.lock - $ git commit -m \"Updated Yarn lockfile\" - $ git push scalingo master - " "https://doc.scalingo.com/languages/javascript/nodejs/#dependencies-installation" - fail - fi -} - fail_yarn_install() { local yarn_engine local log_file="$1" @@ -605,41 +582,6 @@ log_other_failures() { fail fi - if grep -q "npm error code EUSAGE" "$log_file"; then - if grep -q "Please update your lock file" "$log_file"; then - build_data::set_string "failure" "npm-lockfile-out-of-sync" - warn "npm lockfile is not in sync - - This error occurs when the contents of \`package.json\` contains a different - set of dependencies that the contents of \`package-lock.json\`. This can happen - when a package is added, modified, or removed but the lockfile was not updated. - - To fix this, run \`npm install\` locally in your app directory to regenerate the - lockfile, commit the changes to \`package-lock.json\`, and redeploy. - " - fail - fi - fi - - # For now, only capture this error if it doesn't happen during the pruning step. This error shouldn't make - # it past the initial `npm install` (but it can) and it would be nice to see when this type of error slips through. - if grep -q "npm error code ERESOLVE" "$log_file" && [[ "$(build_data::get_current "build_step")" != "prune-dependencies" ]]; then - build_data::set_string "failure" "npm-peer-dependency-conflict" - warn "Conflict detected in requested npm dependencies - - An \`ERESOLVE\` error during installation of npm dependencies means your app contains two or more conflicting - versions of the same dependency. This is typically caused by peer dependency requirements of requested dependencies. - The error above should contain more detail about which dependencies are in conflict. Use tools like \`npm info \` - to get details about a package, including it's peer dependencies. - - The best way to address this issue is to regularly update your dependency versions to prevent conflicts from happening. - - If that is not possible, a temporary solution is to set the \`NPM_CONFIG_LEGACY_PEER_DEPS\` environment variable to \`true\`. - This should be used with caution as ignoring peer dependency conflicts can lead to unexpected runtime errors. - " - fail - fi - if grep -q "ERR_OSSL_EVP_UNSUPPORTED" "$log_file"; then local solution local help_url @@ -695,20 +637,6 @@ log_other_failures() { fail fi - if grep -q "YN0028" "$log_file"; then - build_data::set_string "failure" "yarn-lockfile-out-of-sync" - warn "Yarn lockfile is not in sync - - This error occurs when the contents of \`package.json\` contains a different - set of dependencies that the contents of \`yarn.lock\`. This can happen - when a package is added, modified, or removed but the lockfile was not updated. - - To fix this, run \`yarn install\` locally in your app directory to regenerate the - lockfile, commit the changes to \`yarn.lock\`, and redeploy. - " - fail - fi - # matches the subsequent lines of a stacktrace if grep -q 'at [^ ]* \([^ ]*:\d*\d*\)' "$log_file"; then build_data::set_string "failure" "unknown-stacktrace" diff --git a/lib/binaries.sh b/lib/binaries.sh deleted file mode 100644 index 1399bc5f7..000000000 --- a/lib/binaries.sh +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env bash - -# Compiled from: https://github.com/heroku/buildpacks-nodejs/tree/main/crates/nodejs-data - -install_yarn() { - local dir="$1" - local version=${2:-1.22.x} - local number url code resolve_result - - # npm 12 removed the --unsafe-perm flag and rejects it with EUNKNOWNCONFIG, so only pass it - # to the currently-active npm when that npm still accepts it. - local unsafe_perm=() - if package_managers::npm::supports_unsafe_perm; then - unsafe_perm=(--unsafe-perm) - fi - - if [[ -n "$YARN_BINARY_URL" ]]; then - url="$YARN_BINARY_URL" - echo "Downloading and installing yarn from $url" - else - echo "Downloading and installing yarn ($version)" - if ! package_name=$(determine_yarn_package_name "$version"); then - build_data::set_string "failure" "yarn-resolve-failed" - output::error <<-EOF - Unable to resolve yarn version '$version' via npm info - EOF - false - fi - if ! suppress_output npm install "${unsafe_perm[@]}" --quiet --no-audit --no-progress -g "$package_name@$version"; then - build_data::set_string "failure" "yarn-install-failed" - output::error <<-EOF - Unable to install yarn $version. - Does yarn $version exist? - Is $version valid semver? - Is yarn $version compatible with this Node.js version? - EOF - false - fi - fi - # Verify yarn works before capturing and ensure its stderr is inspectable later - suppress_output yarn --version - if $YARN_2; then - echo "Using yarn $(yarn --version)" - else - echo "Installed yarn $(yarn --version)" - fi -} - -install_pnpm() { - local version="$1" - echo "Downloading and installing pnpm ($version)" - # npm 12 removed the --unsafe-perm flag and rejects it with EUNKNOWNCONFIG, so only pass it - # to the currently-active npm when that npm still accepts it. - local unsafe_perm=() - if package_managers::npm::supports_unsafe_perm; then - unsafe_perm=(--unsafe-perm) - fi - if ! suppress_output npm install "${unsafe_perm[@]}" --quiet --no-audit --no-progress -g "pnpm@$version"; then - build_data::set_string "failure" "pnpm-install-failed" - output::error <<-EOF - Unable to install pnpm $version. - Does pnpm $version exist? - Is $version valid semver? - Is pnpm $version compatible with this Node.js version? - EOF - false - fi - # Verify pnpm works before capturing and ensure its stderr is inspectable later - suppress_output pnpm --version - echo "Using pnpm $(pnpm --version)" -} - -suppress_output() { - local TMP_COMMAND_OUTPUT - TMP_COMMAND_OUTPUT=$(mktemp) - trap "rm -rf '$TMP_COMMAND_OUTPUT' >/dev/null" RETURN - - "$@" >"$TMP_COMMAND_OUTPUT" 2>&1 || { - local exit_code="$?" - cat "$TMP_COMMAND_OUTPUT" - return "$exit_code" - } - return 0 -} - -# Yarn 2+ (aka: "berry") is hosted under a different npm package so we need to do some -# extra checking to determine the correct package name. -determine_yarn_package_name() { - local version="$1" - local NPM_INFO_OUTPUT exit_code - NPM_INFO_OUTPUT=$(mktemp) - - trap "rm -rf '$NPM_INFO_OUTPUT' >/dev/null" RETURN - - npm info "yarn@$version" version >"$NPM_INFO_OUTPUT" 2>&1 - exit_code=$? - - if [[ $exit_code -eq 0 ]]; then - # There are a couple of 2.x versions in the yarn package list, but that should be okay - # since we're using npm to install the binaries. The previous inventory resolver never - # handled this case well. - echo "yarn" - return 0 - fi - - # If nothing is returned for the yarn package list for the given version, it must be @yarnpkg/cli-dist - if grep -q "E404" "$NPM_INFO_OUTPUT"; then - echo "@yarnpkg/cli-dist" - return 0 - fi - - # Handle unexpected output on stderr so it's not captured by command substitution - cat "$NPM_INFO_OUTPUT" >&2 - return "$exit_code" -} diff --git a/lib/dependencies.sh b/lib/dependencies.sh deleted file mode 100644 index f0a71b2ac..000000000 --- a/lib/dependencies.sh +++ /dev/null @@ -1,380 +0,0 @@ -#!/usr/bin/env bash - -list_dependencies() { - local build_dir="$1" - - cd "$build_dir" || return - if $YARN; then - echo "" - (yarn list --depth=0 || true) 2>/dev/null - echo "" - else - (npm ls --depth=0 | tail -n +2 || true) 2>/dev/null - fi -} - -run_if_present() { - local build_dir=${1:-} - local script_name=${2:-} - local has_script_name - local script - - has_script_name=$(has_script "$build_dir/package.json" "$script_name") - script=$(read_json "$build_dir/package.json" ".scripts[\"$script_name\"]") - monitor_name="${script_name//[^[:alnum:]]/_}_script" - - if [[ "$has_script_name" == "true" ]]; then - if $YARN || $YARN_2; then - echo "Running $script_name (yarn)" - # yarn will throw an error if the script is an empty string, so check for this case - if [[ -n "$script" ]]; then - monitor "${monitor_name}" yarn run "$script_name" - fi - elif $PNPM; then - echo "Running $script_name" - monitor "${monitor_name}" pnpm run --if-present "$script_name" - else - echo "Running $script_name" - monitor "${monitor_name}" npm run "$script_name" --if-present - fi - fi -} - -run_build_if_present() { - local build_dir=${1:-} - local script_name=${2:-} - local has_script_name - local script - - has_script_name=$(has_script "$build_dir/package.json" "$script_name") - script=$(read_json "$build_dir/package.json" ".scripts[\"$script_name\"]") - - if [[ "$script" == "ng build" ]]; then - warn "\"ng build\" detected as build script. We recommend you use \`ng build --prod\` or add \`--prod\` to your build flags." - fi - - if [[ "$has_script_name" == "true" ]]; then - if $YARN || $YARN_2; then - echo "Running $script_name (yarn)" - # yarn will throw an error if the script is an empty string, so check for this case - if [[ -n "$script" ]]; then - if [[ -n $NODE_BUILD_FLAGS ]]; then - echo "Running with $NODE_BUILD_FLAGS flags" - monitor "build_script" yarn run "$script_name" "$NODE_BUILD_FLAGS" - else - monitor "build_script" yarn run "$script_name" - fi - fi - elif $PNPM; then - echo "Running $script_name" - if [[ -n $NODE_BUILD_FLAGS ]]; then - echo "Running with $NODE_BUILD_FLAGS flags" - monitor "build_script" pnpm run --if-present "$script_name" -- "$NODE_BUILD_FLAGS" - else - monitor "build_script" pnpm run --if-present "$script_name" - fi - else - echo "Running $script_name" - if [[ -n $NODE_BUILD_FLAGS ]]; then - echo "Running with $NODE_BUILD_FLAGS flags" - monitor "build_script" npm run "$script_name" --if-present -- "$NODE_BUILD_FLAGS" - else - monitor "build_script" npm run "$script_name" --if-present - fi - fi - fi -} - -run_prebuild_script() { - local build_dir=${1:-} - local has_scalingo_prebuild_script - - has_scalingo_prebuild_script=$(has_script "$build_dir/package.json" "scalingo-prebuild") - - if [[ "$has_scalingo_prebuild_script" == "true" ]]; then - header "Prebuild" - run_if_present "$build_dir" 'scalingo-prebuild' - fi -} - -run_build_script() { - local build_dir=${1:-} - local has_build_script has_scalingo_build_script - - has_build_script=$(has_script "$build_dir/package.json" "build") - has_scalingo_build_script=$(has_script "$build_dir/package.json" "scalingo-postbuild") - if [[ "$has_scalingo_build_script" == "true" ]] && [[ "$has_build_script" == "true" ]]; then - echo "Detected both \"build\" and \"scalingo-postbuild\" scripts" - run_if_present "$build_dir" 'scalingo-postbuild' - elif [[ "$has_scalingo_build_script" == "true" ]]; then - run_if_present "$build_dir" 'scalingo-postbuild' - elif [[ "$has_build_script" == "true" && "$NPM_NO_BUILD" != "true" ]]; then - run_build_if_present "$build_dir" 'build' - fi -} - -run_cleanup_script() { - local build_dir=${1:-} - local has_scalingo_cleanup_script - - has_scalingo_cleanup_script=$(has_script "$build_dir/package.json" "scalingo-cleanup") - - if [[ "$has_scalingo_cleanup_script" == "true" ]]; then - header "Cleanup" - run_if_present "$build_dir" 'scalingo-cleanup' - fi -} - -yarn_node_modules() { - local build_dir=${1:-} - local production=${YARN_PRODUCTION:-false} - - echo "Installing node modules (yarn.lock)" - cd "$build_dir" || return - monitor "install_dependencies" yarn install --production="$production" --frozen-lockfile --ignore-engines --prefer-offline 2>&1 -} - -yarn_2_install() { - local build_dir=${1:-} - - echo "Running 'yarn install' with yarn.lock" - cd "$build_dir" || return - - monitor "install_dependencies" yarn install --immutable 2>&1 -} - -yarn_prune_devdependencies() { - local build_dir=${1:-} - local cache_dir=${2:-} - local buildpack_dir=${3:-} - - if [ "$NODE_ENV" == "test" ]; then - echo "Skipping because NODE_ENV is 'test'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$NODE_ENV" != "production" ]; then - echo "Skipping because NODE_ENV is not 'production'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ -n "$YARN_PRODUCTION" ]; then - echo "Skipping because YARN_PRODUCTION is '$YARN_PRODUCTION'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif $YARN_2; then - if [ "$YARN2_SKIP_PRUNING" == "true" ]; then - echo "Skipping because YARN2_SKIP_PRUNING is '$YARN2_SKIP_PRUNING'" - build_data::set_raw "skipped_prune" "true" - return 0 - fi - cd "$build_dir" || return - echo "Running 'yarn heroku prune'" - export YARN_PLUGINS="${buildpack_dir}/yarn2-plugins/prune-dev-dependencies/bundles/@yarnpkg/plugin-prune-dev-dependencies.js" - monitor "prune_dev_dependencies" yarn heroku prune - if node_modules_enabled "$build_dir"; then - echo "Removing local yarn cache to reduce slug size" - rm -rf "$build_dir/.yarn/cache" - fi - build_data::set_raw "skipped_prune" "false" - else - cd "$build_dir" || return - monitor "prune_dev_dependencies" yarn install --frozen-lockfile --ignore-engines --ignore-scripts --prefer-offline 2>&1 - build_data::set_raw "skipped_prune" "false" - fi -} - -has_npm_lock() { - local build_dir=${1:-} - - if [[ -f "$build_dir/package-lock.json" ]] || [[ -f "$build_dir/npm-shrinkwrap.json" ]]; then - echo "true" - else - echo "false" - fi -} - -should_use_npm_ci() { - local build_dir=${1:-} - local npm_version - local major - - npm_version=$(npm --version) - major=$(package_managers::npm::version_major) - - # We should only run `npm ci` if all of the manifest files are there, and we are running at least npm 6.x - # `npm ci` was introduced in the 5.x line in 5.7.0, but this sees very little usage, < 5% of builds - if [[ -f "$build_dir/package.json" ]] && [[ "$(has_npm_lock "$build_dir")" == "true" ]] && (( major >= 6 )); then - echo "true" - else - echo "false" - fi -} - -npm_rebuild() { - local build_dir=${1:-} - local production=${NPM_CONFIG_PRODUCTION:-false} - - # npm 12 removed the --unsafe-perm flag and rejects it with EUNKNOWNCONFIG, so only pass it - # to the currently-active npm when that npm still accepts it. - local unsafe_perm=() - if package_managers::npm::supports_unsafe_perm; then - unsafe_perm=(--unsafe-perm) - fi - - if [ -e "$build_dir/package.json" ]; then - cd "$build_dir" || return - echo "Rebuilding any native modules" - npm rebuild 2>&1 - if [ -e "$build_dir/npm-shrinkwrap.json" ]; then - echo "Installing any new modules (package.json + shrinkwrap)" - else - echo "Installing any new modules (package.json)" - fi - monitor "npm_rebuild" npm install --production="$production" "${unsafe_perm[@]}" --userconfig "$build_dir/.npmrc" 2>&1 - else - echo "Skipping (no package.json)" - fi -} - -npm_prune_devdependencies() { - local npm_version - local build_dir=${1:-} - - npm_version=$(npm --version) - - if [ "$NODE_ENV" == "test" ]; then - echo "Skipping because NODE_ENV is 'test'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$NODE_ENV" != "production" ]; then - echo "Skipping because NODE_ENV is not 'production'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ -n "$NPM_CONFIG_PRODUCTION" ]; then - echo "Skipping because NPM_CONFIG_PRODUCTION is '$NPM_CONFIG_PRODUCTION'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$npm_version" == "5.3.0" ]; then - echo "Skipping because npm 5.3.0 fails when running 'npm prune' due to a known issue" - echo "https://github.com/npm/npm/issues/17781" - echo "" - echo "You can silence this warning by updating to at least npm 5.7.1 in your package.json" - echo "https://doc.scalingo.com/languages/nodejs/start#specifying-a-nodejs-version" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$npm_version" == "5.6.0" ] || - [ "$npm_version" == "5.5.1" ] || - [ "$npm_version" == "5.5.0" ] || - [ "$npm_version" == "5.4.2" ] || - [ "$npm_version" == "5.4.1" ] || - [ "$npm_version" == "5.2.0" ] || - [ "$npm_version" == "5.1.0" ]; then - echo "Skipping because npm $npm_version sometimes fails when running 'npm prune' due to a known issue" - echo "https://github.com/npm/npm/issues/19356" - echo "" - echo "You can silence this warning by updating to at least npm 5.7.1 in your package.json" - echo "https://doc.scalingo.com/languages/nodejs/start#specifying-a-nodejs-version" - build_data::set_raw "skipped_prune" "true" - return 0 - else - cd "$build_dir" || return - monitor "prune_dev_dependencies" npm prune --userconfig "$build_dir/.npmrc" 2>&1 - build_data::set_raw "skipped_prune" "false" - fi -} - -pnpm_prune_devdependencies() { - local build_dir=${1:-} - - cd "$build_dir" || return - - if [ "$NODE_ENV" == "test" ]; then - echo "Skipping because NODE_ENV is 'test'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$NODE_ENV" != "production" ]; then - echo "Skipping because NODE_ENV is not 'production'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [ "$PNPM_SKIP_PRUNING" == "true" ]; then - echo "Skipping because PNPM_SKIP_PRUNING is '$PNPM_SKIP_PRUNING'" - build_data::set_raw "skipped_prune" "true" - return 0 - elif [[ "$(pnpm_workspace_configured "$build_dir")" == "true" ]]; then - # Get pnpm projects - mapfile -t project_paths < <(list_pnpm_workspace_projects) - # Check if any projects contain lifecycle scripts, and skip pruning if true - for project_path in "${project_paths[@]}"; do - if has_pnpm_lifecycle_script "$project_path/package.json"; then - warn_skipping_unsafe_pnpm_workspace_prune "$project_path" - build_data::set_raw "skipped_prune" "true" - return 0 - fi - done - # Remove node_modules from each project - for project_path in "${project_paths[@]}"; do - rm -rf "$project_path/node_modules" - done - # Reinstall with production-only dependencies - monitor "prune_dev_dependencies" pnpm install --prod --frozen-lockfile 2>&1 - build_data::set_raw "skipped_prune" "false" - return 0 - fi - - pnpm_version=$(pnpm --version) - pnpm_major_version=$(echo "$pnpm_version" | cut -d "." -f 1) - pnpm_minor_version=$(echo "$pnpm_version" | cut -d "." -f 2) - pnpm_patch_version=$(echo "$pnpm_version" | cut -d "." -f 3) - - pnpm_prune_args=("prune" "--prod") - - # prior to 8.15.6, pnpm prune would execute lifecycle scripts such as `preinstall` and `postinstall` - # so we should check if we're on that version + there are lifecycle scripts registered and, if so, - # we'll let the user know that pruning can't be done safely so we're skipping it - if (( "$pnpm_major_version" < 8 )) || \ - (( "$pnpm_major_version" == 8 && "$pnpm_minor_version" < 15 )) || \ - (( "$pnpm_major_version" == 8 && "$pnpm_minor_version" == 15 && "$pnpm_patch_version" < 6)); then - if has_pnpm_lifecycle_script "$build_dir/package.json"; then - warn_skipping_unsafe_pnpm_prune "$pnpm_version" - build_data::set_raw "skipped_prune" "true" - return - fi - else - # we're on a version that supports this flag (8.15.6 and higher) - pnpm_prune_args+=("--ignore-scripts") - fi - - monitor "prune_dev_dependencies" pnpm "${pnpm_prune_args[@]}" 2>&1 - - build_data::set_raw "skipped_prune" "false" -} - -pnpm_workspace_configured() { - local build_dir=${1:-} - local workspace_file="$build_dir/pnpm-workspace.yaml" - local result - - if [[ -f "$workspace_file" ]]; then - # prior to pnpm 10.5.0, the `packages` key was mandatory, but now, you can store - # other pnpm-related config settings in `pnpm-workspace.yaml`. - result=$(read_yaml "$workspace_file" '.packages') - - if [[ -n "$result" && "$result" != "null" ]]; then - echo "true" - return - fi - fi - - echo "false" -} - -has_pnpm_lifecycle_script() { - local package_json=$1 - # the following are lifecycle scripts that will execute on install/prune by pnpm - [[ -f "$package_json" ]] && \ - jq -e '.scripts | (has("pnpm:devPreinstall") or has("preinstall") or has("install") or has("postinstall") or has("prepare"))' \ - "$package_json" > /dev/null 2>&1 -} - -list_pnpm_workspace_projects() { - pnpm list --recursive --json --depth -1 2>/dev/null | jq -r '.[].path' -} diff --git a/lib/environment.sh b/lib/environment.sh index c35a8db29..1dc8d3298 100644 --- a/lib/environment.sh +++ b/lib/environment.sh @@ -4,20 +4,6 @@ get_os() { uname | tr '[:upper:]' '[:lower:]' } -get_cpu() { - if [[ "$(uname -p)" = "i686" ]]; then - echo "x86" - else - echo "x64" - fi -} - -get_platform() { - os=$(get_os) - cpu=$(get_cpu) - echo "$os-$cpu" -} - create_default_env() { local YARN=$1 diff --git a/lib/failures.sh b/lib/failures.sh index 67bea3795..c3277e2f5 100644 --- a/lib/failures.sh +++ b/lib/failures.sh @@ -55,6 +55,24 @@ function failure::emit() { fail } +# Emits a buildpack-classified failure for the case where the tool inside a `tool | tee log` +# pipeline exited 0 but a downstream stage (typically `tee` writing to the log) failed — for +# example the build ran out of disk space. Callers pass a stable failure id, the PIPESTATUS +# array joined as a string (typically "${pipe_status[*]}") for observability, and the +# user-facing message. Records classification=buildpack and detail=PIPESTATUS=[...]. +function failure::handle_pipefail() { + local id="${1}" + local pipe_status="${2}" + local message="${3}" + local -A failure=( + [id]="${id}" + [classification]="buildpack" + [detail]="PIPESTATUS=[${pipe_status}]" + [message]="${message}" + ) + failure::emit failure +} + # Restore the sourcing shell's original options (see preamble) so strict mode doesn't leak # into un-migrated callers. errexit/nounset come from the saved `$-`; pipefail from its own # saved `set +o` line. diff --git a/lib/features.sh b/lib/features.sh deleted file mode 100644 index 42ffb0a4c..000000000 --- a/lib/features.sh +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/env bash - -# This module is designed to be able to roll out features to a -# random segment of users for A/B testing. This takes as input a -# list of features along with % chance they will be enabled, -# decides which to enable, and persists these decisions into the -# application cache. -# -# This module takes in no outside data, so it is limited in it's -# uses. While a feature can be persisted between builds for the -# same app, it cannot be consistent for a given user / team. Even -# different PR apps will be decided independently. -# -# This means that this should not be used for changing the build -# behavior of the buildpack. Builds should always work consistently -# no matter what features are turned on or off. -# -# Where this module can be useful is when deciding between two -# identical behaviors that may have performance trade-offs, or -# testing the efficacy of different messaging. -# -# Examples: -# testing two different caching strategies against each other -# showing guidance on a particular type of failure -# -# It is expected that these features will be used for roll-outs -# and be short-lived -# -# ** Schema ** -# -# This module expects a "schema" file as input. This is used to -# make sure that all current features are documented in one -# place. The file is a list of key=value pairs on individual -# lines. -# -# The key is the name, and the value is an integery between 0 and -# 100 inclusive that represents the likelyhood that the feature -# will be turned on for any given app. -# -# Example: -# ``` -# always-on=100 // this will always be turned on, not super useful -# ab-test=50 // this will be split 50/50 -# small-test=5 // this will be turned on for 5% of apps -# ``` -# -# ** Invalidating features ** -# -# Any time the schema file contents change, the existing feature -# assignments will be invalidated and re-assigned -# -# ** Testing ** -# -# It would be frustrating if it wasn't clear when CI was running which -# branch was being used, or if tests were flaky because they were choosing -# different features for each run. -# -# To that end, there is a special file that can be included in test fixtures -# to hard-code features to turn on and off. -# -# $BUILD_DIR/heroku-buildpack-features -# -# Example: -# ``` -# feature-a=true -# feature-b=true -# ``` -# -# An empty file can be included as part of test code to default all tests -# to off -# -# Once a feature is in production, checking this file into your app -# can allow you to choose which features you want an app to use. - -# variables shared by this whole module -FEATURES_DATA_FILE="" -OVERRIDE_FILE="" - -features_init() { - local name="$1" - local build_dir="$2" - local cache_dir="$3" - local schema="$4" - - local last_schema_hash schema_hash random odds hash_file - - FEATURES_DATA_FILE="$cache_dir/features/$name" - hash_file="$cache_dir/features/$name-hash" - OVERRIDE_FILE="$build_dir/heroku-buildpack-features" - - mkdir -p "$cache_dir/features/" - touch "$hash_file" - - last_schema_hash="$(cat "$hash_file")" - schema_hash="$(sha1sum "$schema" | awk '{ print $1 }')" - - # If the schema has changed, blow away the current values - # and start fresh. This is essentially "wiping the slate clean" - # and no previous features will be enabled for anyone - # - # In the case that the schema hash is the same, we keep - # all of the previously decided features (file is the same) - # and decide on any new ones - if [[ "$last_schema_hash" != "$schema_hash" ]]; then - kv_create "$FEATURES_DATA_FILE" - kv_clear "$FEATURES_DATA_FILE" - # save out the hash we're using to generate this set of features - echo "$schema_hash" > "$hash_file" - fi - - # iterate through the schema and decide if each new feature - # should be turned on or not - kv_keys "$schema" | tr ' ' '\n' | while read -r key; do - if [[ -n "$key" ]]; then - if [[ -n "$(kv_get "$FEATURES_DATA_FILE" "$key")" ]]; then - continue - else - # generate a random number between 0 and 100 - random=$((RANDOM % 100)) - # the value in the schema should be a number between 0 and 100 inclusive - odds=$(kv_get "$schema" "$key") - if [[ "$random" -lt "$odds" ]]; then - kv_set "$FEATURES_DATA_FILE" "$key" "true" - else - kv_set "$FEATURES_DATA_FILE" "$key" "false" - fi - fi - fi - done -} - -# Determine whether an feature is enabled or disabled -# Must call features_init first, otherwise only "false" will be returned -# -# Possible outputs: "true" "false" -features_get() { - local result - if [[ -f "$OVERRIDE_FILE" ]]; then - result=$(kv_get "$OVERRIDE_FILE" "$1") - else - result=$(kv_get "$FEATURES_DATA_FILE" "$1") - fi - if [[ "$result" == "true" ]]; then - echo "true" - else - echo "false" - fi -} - -features_get_with_blank() { - local result - if [[ -f "$OVERRIDE_FILE" ]]; then - result=$(kv_get "$OVERRIDE_FILE" "$1") - else - result=$(kv_get "$FEATURES_DATA_FILE" "$1") - fi - if [[ "$result" == "true" ]]; then - echo "true" - elif [[ "$result" == "false" ]]; then - echo "false" - else - echo "" - fi -} - -# Outputs a list of feature names, one-per-line -features_list() { - if [[ -f "$OVERRIDE_FILE" ]]; then - kv_keys "$OVERRIDE_FILE" - else - kv_keys "$FEATURES_DATA_FILE" - fi -} - -# Force an feature to be turned on or off -# This is expected to be used during development -features_override() { - local name="$1" - local value="$2" - - if [[ "$value" == "true" ]]; then - kv_set "$FEATURES_DATA_FILE" "$name" "true" - else - kv_set "$FEATURES_DATA_FILE" "$name" "false" - fi -} diff --git a/lib/json.sh b/lib/json.sh index 0852af012..3ce2644c1 100644 --- a/lib/json.sh +++ b/lib/json.sh @@ -16,18 +16,6 @@ read_json() { fi } -json_has_key() { - local file="$1" - local key="$2" - - if test -f "$file"; then - # shellcheck disable=SC2002 - cat "$file" | jq ". | has(\"$key\")" - else - echo "false" - fi -} - has_script() { local file="$1" local key="$2" diff --git a/lib/kvstore.sh b/lib/kvstore.sh deleted file mode 100644 index b5bf1c23d..000000000 --- a/lib/kvstore.sh +++ /dev/null @@ -1,73 +0,0 @@ -#!/usr/bin/env bash - -kv_create() { - local f=$1 - mkdir -p "$(dirname "$f")" - touch "$f" -} - -kv_clear() { - local f=$1 - echo "" > "$f" -} - -kv_set() { - if [[ $# -eq 3 ]]; then - local f=$1 - if [[ -f $f ]]; then - echo "$2=$3" >> "$f" - fi - fi -} - -kv_get() { - if [[ $# -eq 2 ]]; then - local f=$1 - if [[ -f $f ]]; then - grep "^$2=" "$f" | sed -e "s/^$2=//" | tail -n 1 || true - fi - fi -} - -# get the value, but wrap it in quotes if it contains a space -kv_get_escaped() { - local value - value=$(kv_get "$1" "$2") - if [[ $value =~ [[:space:]]+ ]]; then - echo "\"$value\"" - else - echo "$value" - fi -} - -kv_keys() { - local f=$1 - local keys=() - - if [[ -f $f ]]; then - # Iterate over each line, splitting on the '=' character - # - # The || [[ -n "$key" ]] statement addresses an issue with reading the last line - # of a file when there is no newline at the end. This will not happen if the file - # is created with this module, but can happen if it is written by hand. - # See: https://stackoverflow.com/questions/12916352/shell-script-read-missing-last-line - while IFS="=" read -r key value || [[ -n "$key" ]]; do - # if there are any empty lines in the store, skip them - if [[ -n $key ]]; then - keys+=("$key") - fi - done < "$f" - - echo "${keys[@]}" | tr ' ' '\n' | sort -u - fi -} - -kv_list() { - local f=$1 - - kv_keys "$f" | tr ' ' '\n' | while read -r key; do - if [[ -n $key ]]; then - echo "$key=$(kv_get_escaped "$f" "$key")" - fi - done -} diff --git a/lib/output.sh b/lib/output.sh index 7d290e369..b18aced5d 100644 --- a/lib/output.sh +++ b/lib/output.sh @@ -31,15 +31,6 @@ header() { echo "-----> $*" || true } -bright_header() { - echo "" || true - echo -e "\033[1;33m-----> $* \033[0m" -} - -header_skip_newline() { - echo "-----> $*" || true -} - error() { echo " ! $*" >&2 || true echo "" || true diff --git a/lib/package_manager.sh b/lib/package_manager.sh new file mode 100644 index 000000000..512a318e4 --- /dev/null +++ b/lib/package_manager.sh @@ -0,0 +1,190 @@ +#!/usr/bin/env bash + +# Enable strict mode for ShellCheck but restore the caller's options at the end of the file +# (see epilogue) so they don't bleed into un-migrated scripts that source this lib. The +# caller's flags are read from `$-` (the current shell); a `$(set +o)` capture runs in a +# command-substitution subshell where bash always forces errexit off, so it would later +# restore errexit as disabled even when the caller had it on. pipefail has no `$-` letter, so +# it is captured separately (it is reported correctly inside command substitution). +# shellcheck disable=SC2034 # both are consumed by the epilogue +__package_manager_saved_flags="$-" +__package_manager_saved_pipefail="$(set +o | grep pipefail)" +set -euo pipefail + +# Dispatches a named lifecycle script to the active package manager's module, which spells and +# runs the tool-specific command (`npm/yarn/pnpm run