Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Corepack support (Yarn only) #1222

Merged
merged 1 commit into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@

## [Unreleased]

- Support [Corepack](https://nodejs.org/api/corepack.html) installation of [Yarn](https://yarnpkg.com/) ([#1222](https://github.com/heroku/heroku-buildpack-nodejs/pull/1222))

## [v241] - 2024-04-04

- Added Node.js version 21.7.2.
- Added Node.js version 20.12.1.
- Added Node.js version 18.20.1.
Expand Down
54 changes: 39 additions & 15 deletions bin/compile
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,14 @@ if [[ "$YARN_2" == "true" ]]; then
# get yarn_path
VENDOR_PATH=$(get_yarn_path "$BUILD_DIR")

# fail for no yarnPath in rc
fail_missing_yarn_path "$BUILD_DIR" "$VENDOR_PATH"

# fail for missing yarn in .yarn/releases
fail_missing_yarn_vendor "$BUILD_DIR" "$VENDOR_PATH"
# if we're installing yarn via corepack we don't need to fail
# on the yarn path configuration or vendored script
if [[ "$(read_json "$BUILD_DIR/package.json" ".packageManager")" != yarn* ]]; then
# fail for no yarnPath in rc
fail_missing_yarn_path "$BUILD_DIR" "$VENDOR_PATH"
# fail for missing yarn in .yarn/releases
fail_missing_yarn_vendor "$BUILD_DIR" "$VENDOR_PATH"
fi
fi

### Configure package manager cache directories
Expand All @@ -196,25 +199,41 @@ fi
export YARN_CACHE_FOLDER NPM_CONFIG_CACHE

install_bins() {
local node_engine npm_engine yarn_engine npm_version node_version
local node_engine npm_engine yarn_engine node_version package_manager

node_engine=$(read_json "$BUILD_DIR/package.json" ".engines.node")
npm_engine=$(read_json "$BUILD_DIR/package.json" ".engines.npm")
yarn_engine=$(read_json "$BUILD_DIR/package.json" ".engines.yarn")
package_manager=$(read_json "$BUILD_DIR/package.json" ".packageManager")

meta_set "node-version-request" "$node_engine"
meta_set "npm-version-request" "$npm_engine"
meta_set "yarn-version-request" "$yarn_engine"
meta_set "package-manager-request" "$package_manager"

echo "engines.node (package.json): ${node_engine:-unspecified}"
echo "engines.npm (package.json): ${npm_engine:-unspecified (use default)}"

echo "engines.node (package.json): ${node_engine:-unspecified}"
echo "engines.npm (package.json): ${npm_engine:-unspecified (use default)}"
if $YARN || [ -n "$yarn_engine" ]; then
echo "engines.yarn (package.json): ${yarn_engine:-unspecified (use default)}"
echo "engines.yarn (package.json): ${yarn_engine:-unspecified (use default)}"
fi

if [ -n "$package_manager" ]; then
echo "packageManager (package.json): $(echo "$package_manager" | cut -d "+" -f 1)"
fi

echo ""

warn_node_engine "$node_engine"

if [ -n "$yarn_engine" ] && [[ "$package_manager" == yarn* ]]; then
warn_multiple_yarn_version "$package_manager" "$yarn_engine"
fi

if has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then
warn_yarn_release_script_with_package_manager "$package_manager" "$(get_yarn_path "$BUILD_DIR")"
fi

meta_set "build-step" "install-nodejs"
monitor "install-node-binary" install_nodejs "$node_engine" "$BUILD_DIR/.heroku/node"

Expand All @@ -225,12 +244,17 @@ install_bins() {
mcount "version.node.$node_version"
meta_set "node-version" "$node_version"

# Download yarn if there is a yarn.lock file or if the user
# has specified a version of yarn under "engines". We'll still
# only install using yarn if there is a yarn.lock file
if $YARN || [ -n "$yarn_engine" ]; then
meta_set "build-step" "install-yarn"
monitor "install-yarn-binary" install_yarn "$BUILD_DIR/.heroku/yarn" "$yarn_engine"
if ! has_release_script "$BUILD_DIR" && [[ "$package_manager" == yarn* ]]; then
meta_set "build-step" "install-yarn-using-corepack"
monitor "install-yarn-using-corepack" install_yarn_using_corepack_package_manager "$package_manager" "$node_version"
else
# Download yarn if there is a yarn.lock file or if the user
# has specified a version of yarn under "engines". We'll still
# only install using yarn if there is a yarn.lock file
if $YARN || [ -n "$yarn_engine" ]; then
meta_set "build-step" "install-yarn"
monitor "install-yarn-binary" install_yarn "$BUILD_DIR/.heroku/yarn" "$yarn_engine"
fi
fi

if $YARN; then
Expand Down
52 changes: 52 additions & 0 deletions lib/binaries.sh
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,58 @@ install_npm() {
fi
}

install_yarn_using_corepack_package_manager() {
local package_manager="$1"
local node_version="$2"
install_corepack_package_manager "$package_manager" "$node_version"
suppress_output yarn --version
echo "Using yarn $(yarn --version)"
}

install_corepack_package_manager() {
colincasey marked this conversation as resolved.
Show resolved Hide resolved
local node_major_version
local node_minor_version

local package_manager="$1"
local node_version="$2"

node_major_version=$(echo "$node_version" | cut -d "." -f 1 | sed 's/^v//')
node_minor_version=$(echo "$node_version" | cut -d "." -f 2)

# Corepack is available in: v16.9.0, v14.19.0
if (( node_major_version >= 17 )) || (( node_major_version == 14 && node_minor_version >= 19 )) || (( node_major_version >= 16 && node_minor_version >= 9 )); then
suppress_output corepack --version
corepack_version=$(corepack --version)
corepack enable 2>&1

# The Corepack CLI interface was refactored in 0.20, before that the `install` command was called `prepare` and it
# doesn't support the --global argument - https://github.com/nodejs/corepack/blob/main/CHANGELOG.md#0200-2023-08-29
corepack_major_version=$(echo "$corepack_version" | cut -d "." -f 1)
corepack_minor_version=$(echo "$corepack_version" | cut -d "." -f 2)
if (( corepack_major_version == 0 )) && (( corepack_minor_version < 20 )); then
corepack_install_command="prepare"
corepack_install_args=()
else
corepack_install_command="install"
corepack_install_args=("--global")
fi

echo "Installing $(echo "$package_manager" | cut -d "+" -f 1) via corepack ${corepack_version}"
install_output=$(mktemp)
if ! corepack "${corepack_install_args[@]}" "$corepack_install_command" "$package_manager" > "$install_output" 2>&1; then
# always show the output on error
cat "$install_output"
if grep --ignore-case "mismatch hashes" "$install_output"; then
fail_corepack_install_invalid_hash "$package_manager"
else
fail_corepack_install_invalid_version "$package_manager"
fi
fi
else
fail_corepack_not_available "$package_manager" "$node_version"
fi
}

suppress_output() {
local TMP_COMMAND_OUTPUT
TMP_COMMAND_OUTPUT=$(mktemp)
Expand Down
98 changes: 98 additions & 0 deletions lib/failure.sh
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,25 @@ fail_missing_yarn_vendor() {
fi
}

fail_corepack_not_available() {
local package_manager="$1"
local node_version="$2"

mcount "failures.corepack-unsupported"
meta_set "failure" "failures.corepack-unsupported"
header "Build failed"
warn "Corepack is not supported in Node.js $node_version

Your application indicated that $package_manager should be installed using Corepack. This feature
is included with all Node.js releases starting from Node.js 14.19.0 / 16.9.0. The version
of Node.js used in this build is $node_version which does not support Corepack.

To use Corepack, update your Node.js version:
joshwlewis marked this conversation as resolved.
Show resolved Hide resolved
https://devcenter.heroku.com/articles/nodejs-support#specifying-a-node-js-version
"
fail
}

log_other_failures() {
local log_file="$1"

Expand Down Expand Up @@ -840,3 +859,82 @@ warn_unmet_dep() {
mcount 'warnings.modules.unmet'
fi
}

warn_multiple_yarn_version() {
local package_manager="$1"
local yarn_engine="$2"
warn "Multiple Yarn versions declared

The package.json file indicates the target version of Yarn to install in two fields:
- \"packageManager\": \"$package_manager\"
- \"engines.yarn\": \"$yarn_engine\"

If both fields are present, then \"packageManager\" will take precedence and \"$package_manager\" will be installed.

To ensure we install the version of Yarn you want, remove one of these fields."
mcount 'warnings.yarn.multiple-version'
}

warn_yarn_release_script_with_package_manager() {
local package_manager="$1"
local release_script="$2"
warn "Yarn release script may conflict with \"packageManager\"

The package.json file indicates the target version of Yarn to install with:
- \"packageManager\": \"$package_manager\"

But the .yarnrc.yml configuration indicates a vendored release of Yarn should be used with:
- yarnPath: \"$release_script\"

This will cause the buildpack to install $package_manager but, when running Yarn commands, the vendored release
at \"$release_script\" will be executed instead.

To ensure we install the version of Yarn you want, choose only one of the following actions:
- Remove the \"packageManager\" field from package.json
- Remove the \"yarnPath\" configuration from .yarnrc.yml and delete the vendored release at \"$release_script\""
mcount 'warnings.yarn.release-script-with-package-manager'
}

fail_corepack_install_invalid_hash() {
local package_manager="$1"
package_manager_name=$(echo "$package_manager" | cut -d "@" -f 1)
package_manager_version=$(echo "$package_manager" | cut -d "@" -f 2 | cut -d "+" -f 1)
package_manager_hash=$(echo "$package_manager" | cut -d "@" -f 2 | cut -d "+" -f 2)

mcount "failures.corepack-install.hash"
meta_set "failure" "failures.corepack-install.hash"
header "Build failed"
warn "Error installing $package_manager_name version $package_manager_version

The hash provided for the $package_manager_name version declared in package.json ($package_manager_hash) is incorrect.

To correct this, run the following command:

> corepack use $package_manager_name@$package_manager_version

Then commit and push the changes to package.json." \
"https://devcenter.heroku.com/articles/nodejs-support#specifying-a-$package_manager_name-version"
fail
}

fail_corepack_install_invalid_version() {
local package_manager="$1"
package_manager_name=$(echo "$package_manager" | cut -d "@" -f 1)
package_manager_version=$(echo "$package_manager" | cut -d "@" -f 2 | cut -d "+" -f 1)

mcount "failures.corepack-install.version"
meta_set "failure" "failures.corepack-install.version"
header "Build failed"
warn "Error installing $package_manager_name version $package_manager_version

Can’t find the $package_manager_name version that matches the requested version declared in package.json ($package_manager_version).

Verify that the requested version range matches a published version of $package_manager_name by checking
https://www.npmjs.com/package/$package_manager_name?activeTab=versions or trying the following command:

> npm show '$package_manager' versions

Update the version specified field in package.json to a published $package_manager_name version" \
"https://devcenter.heroku.com/articles/nodejs-support#specifying-a-$package_manager_name-version"
fail
}
7 changes: 7 additions & 0 deletions lib/yarn-2.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ detect_yarn_2() {
fi
}

has_release_script() {
local build_dir="$1"
local yarn_path
yarn_path=$($YQ r "$build_dir/.yarnrc.yml" yarnPath 2>&1)
[[ -n "$yarn_path" ]] && [ -f "$build_dir/$yarn_path" ]
}

has_yarn_cache() {
local build_dir="$1"
local yarn_cache="$build_dir/.yarn/cache"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.
16 changes: 16 additions & 0 deletions test/fixtures/corepack-invalid-package-manager-sha/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "corepack-test-app",
"version": "0.0.1",
"description": "corepack availability test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "20.9.0"
},
"scripts": {
"start": "node foo.js"
},
"packageManager": "yarn@3.2.3+sha224.BADSHA"
}
1 change: 1 addition & 0 deletions test/fixtures/corepack-invalid-package-manager/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.
16 changes: 16 additions & 0 deletions test/fixtures/corepack-invalid-package-manager/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "corepack-test-app",
"version": "0.0.1",
"description": "corepack availability test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "20.9.0"
},
"scripts": {
"start": "node foo.js"
},
"packageManager": "yarn@999.999.999"
}
1 change: 1 addition & 0 deletions test/fixtures/corepack-node-14.18/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.
16 changes: 16 additions & 0 deletions test/fixtures/corepack-node-14.18/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "corepack-test-app",
"version": "0.0.1",
"description": "corepack availability test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "14.18.0"
},
"scripts": {
"start": "node foo.js"
},
"packageManager": "yarn@2.2.2"
}
1 change: 1 addition & 0 deletions test/fixtures/corepack-node-14.19/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.
16 changes: 16 additions & 0 deletions test/fixtures/corepack-node-14.19/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "corepack-test-app",
"version": "0.0.1",
"description": "corepack availability test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "14.19.0"
},
"scripts": {
"start": "node foo.js"
},
"packageManager": "yarn@2.2.2"
}
1 change: 1 addition & 0 deletions test/fixtures/corepack-node-15/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.
16 changes: 16 additions & 0 deletions test/fixtures/corepack-node-15/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"name": "corepack-test-app",
"version": "0.0.1",
"description": "corepack availability test app",
"repository" : {
"type" : "git",
"url" : "http://github.com/example/example.git"
},
"engines": {
"node": "15.x"
},
"scripts": {
"start": "node foo.js"
},
"packageManager": "yarn@2.2.2"
}
1 change: 1 addition & 0 deletions test/fixtures/corepack-node-16.8/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A fake README, to keep npm from polluting stderr.