# Copyright 2026 Gentoo Authors # Distributed under the terms of the GNU General Public License v2 # @ECLASS: npm.eclass # @MAINTAINER: # ingenarel # @AUTHOR: # ingenarel # @BLURB: eclass for creating packages that uses npm modules case ${EAPI} in 8|9) ;; *) die "${ECLASS}: EAPI ${EAPI:-0} not supported" ;; esac if [[ -z ${_NPM_ECLASS} ]]; then _NPM_ECLASS=1 BDEPEND="net-libs/nodejs[npm]" # @ECLASS_VARIABLE: NPM_MODULES # @DEFAULT_UNSET # @PRE_INHERIT # @DESCRIPTION: # Bash array containing all modules that are to be downloaded. # @ECLASS_VARIABLE: MAPPED_MODULES # @DEFAULT_UNSET # @PRE_INHERIT # @DESCRIPTION: # Bash assosiative array containing all modules that can't easily be put into NPM_MODULES either because it's not using # the npm registry, or because it has a custom name, that doesn't match the name it will get unpacked to # @ECLASS_VARIABLE: NPM_MODULES_BINS_SYMLINK # @DEFAULT_UNSET # @PRE_INHERIT # @DESCRIPTION: # Bash assosiative array containing all key value pairs for .bin files, key is the symlink target, and the value is # the symlink destination # @ECLASS_VARIABLE: NPM_MODULES_URI # @OUTPUT_VARIABLE # @DESCRIPTION: # List of URIs to put in SRC_URI created from NPM_MODULES variable. # @FUNCTION: npm_get_node_modules_dir # @DESCRIPTION: # gets the node_modules dir # it's a function instead of a variable so that creating ebuilds for projects that use submodules can be installed # properly npm_get_node_modules_dir() { echo "${S}/node_modules" } # @FUNCTION: _npm_safelink # @INTERNAL # @DESCRIPTION: # safely create symlinks _npm_safelink() { local parent_path parent_path="$(realpath --canonicalize-missing "$2/.." || die "getting realpath failed for ${parent_path}" )" if [[ ! -d "${parent_path}" ]]; then mkdir --parents "${parent_path}"\ || die "creating parents directories failed" fi if [[ -h "$2" ]] && [[ "$( realpath --canonicalize-missing "$2" || die "getting realpath failed for ${2}" )" == "$( realpath --canonicalize-missing "$1" || die "getting realpath failed for ${1}" )" ]]; then : else ln -sf "$1" "$2"\ || die "trying to symlink failed" fi } # @FUNCTION: _npm_set_modules_uris # @INTERNAL # @DESCRIPTION: # Generates the URIs from NPM_MODULES to put in SRC_URI to help fetch dependencies. # The value is set as NPM_MODULES_URIS. _npm_set_modules_uris() { for module in "${NPM_MODULES[@]}"; do local full_project_name="${module%%|*}" local full_project_name="${full_project_name##*node_modules/}" local version_name="${module##*|}" local module_name="${module##*/}" local module_name="${module_name%%|*}" NPM_MODULES_URI+=" https://registry.npmjs.org/${full_project_name}/-/${module_name}-${version_name}.tgz -> ${full_project_name//\//-}-${version_name}-NPM_MODULE.tgz " done for module in "${MAPPED_MODULES[@]}"; do NPM_MODULES_URI+=" ${module}" done } _npm_set_modules_uris # @FUNCTION: _npm_unpack_module # @INTERNAL # @DESCRIPTION: # unpacks a single module _npm_unpack_module() { local module_name="$1" local module_path="$2" local full_directory_name full_directory_name="$(npm_get_node_modules_dir)/${module_name%%|*}" local full_project_name="${full_directory_name##*node_modules/}" local archive_file if [[ -z "${module_path}" ]]; then local version_name="${module_name##*|}" archive_file="${DISTDIR}/${full_project_name//\//-}-${version_name}-NPM_MODULE.tgz" else archive_file="${DISTDIR}/${module_path}" fi if [[ ! -e "${archive_file}" ]]; then die "archive file doesn't exist" fi mkdir --parents "${full_directory_name}" || die "creating dirname failed" tar --extract \ --directory "${full_directory_name}" \ --file "${archive_file}" \ --strip-components=1 || die "extracting failed" } # @FUNCTION: npm_unpack_modules # @DESCRIPTION: # Unpacks the NPM modules npm_unpack_modules() { for module in "${NPM_MODULES[@]}"; do _npm_unpack_module "${module}" done for module in "${!MAPPED_MODULES[@]}"; do _npm_unpack_module "${module}" "${MAPPED_MODULES[${module}]##*$'\t'}" done for target in "${!NPM_MODULES_BINS_SYMLINK[@]}"; do local destination destination="$(npm_get_node_modules_dir)/.bin/${NPM_MODULES_BINS_SYMLINK[${target}]}" if [[ "${target}" == *'/node_modules/'* ]]; then destination="$(npm_get_node_modules_dir)/${target%%/node_modules/*}/node_modules/.bin/${NPM_MODULES_BINS_SYMLINK[${target}]}" fi _npm_safelink "$(npm_get_node_modules_dir)/${target}" "${destination}" chmod +x "$(npm_get_node_modules_dir)/${target}" || die "writing exec perms to $(npm_get_node_modules_dir)/${target} failed" chmod +x "${destination}" || die "writing exec perms to ${destination} failed" done } # @FUNCTION: npm_src_unpack # @DESCRIPTION: # Unpacks the NPM modules npm_src_unpack() { for archive in ${A}; do case "${archive}" in *-NPM_MODULE.tgz) ;; *-deps.tar*) tar --extract \ --directory "${S}" \ --file "${DISTDIR}/${archive}" || die "extracting failed" ;; *) unpack "${archive}" ;; esac done npm_unpack_modules } # @FUNCTION: npm_patch_package_json # @DESCRIPTION: # patches the package.json if needed npm_patch_package_json(){ local dependencies_started=false local deps_to_bundle="" local package_json_file="package.json" while IFS='' read -r line; do case "${line}" in *[[:space:]]'"dependencies":'[[:space:]]*'{'*) dependencies_started=true ;; *[[:space:]]'}'*) dependencies_started=false ;; *) line="\"${line#*\"}" ${dependencies_started} && deps_to_bundle+=" ${line%:*}" ;; esac done < "${package_json_file}" deps_to_bundle="${deps_to_bundle//\" /\",$'\n'}" dependencies_printed=false while IFS='' read -r line; do case "${line}" in *[[:space:]]'"bundledDependencies":'[[:space:]]*'['*) printf '%s\n' "${line}" printf '%s,\n' "${deps_to_bundle}" dependencies_printed=true ;; *) printf '%s\n' "${line}" ;; esac done < "${package_json_file}" > "${package_json_file}.new" if ! ${dependencies_printed}; then echo '' > "${package_json_file}.new" while IFS='' read -r line; do case "${line}" in *[[:space:]]'"dependencies":'[[:space:]]*'{'*) printf '\t"bundledDependencies": [\n%s\n],\n' "${deps_to_bundle}" dependencies_printed=true printf '%s\n' "${line}" ;; *[[:space:]]'}'*) printf '%s\n' "${line}" ;; *) printf '%s\n' "${line}" ;; esac done < "${package_json_file}" > "${package_json_file}.new" fi mv "${package_json_file}.new" "${package_json_file}" } # @FUNCTION: npm_src_prepare # @DESCRIPTION: # runs default prepare and patches the package.json if needed npm_src_prepare(){ default npm_patch_package_json } # @FUNCTION: npm_build # @DESCRIPTION: # runs npm build npm_build() { npm --offline --verbose run build "${@}" || die "npm build failed" } # @FUNCTION: npm_src_install # @DESCRIPTION: # installs the npm project npm_src_install() { npm \ --offline \ --global \ --verbose \ --install-links \ --prefix "${ED}"/usr \ install "${@}" || die "npm install failed" } # @FUNCTION: npm_src_test # @DESCRIPTION: # run tests npm_src_test() { npm --offline --verbose test || die "npm tests failed" } # @FUNCTION: enpm # @DESCRIPTION: # run npm commands in a safe way enpm() { npm --offline --verbose "${@}" || die "running npm --offline --verbose ${*} failed" } fi EXPORT_FUNCTIONS src_unpack src_prepare src_install src_test