diff --git a/pre_commit/languages/node.py b/pre_commit/languages/node.py index af7dc6f8..c89d78cc 100644 --- a/pre_commit/languages/node.py +++ b/pre_commit/languages/node.py @@ -2,6 +2,7 @@ from __future__ import annotations import contextlib import functools +import json import os import sys from collections.abc import Generator @@ -17,6 +18,7 @@ from pre_commit.languages.python import bin_dir from pre_commit.prefix import Prefix from pre_commit.util import cmd_output from pre_commit.util import cmd_output_b +from pre_commit.util import get_npm_version from pre_commit.util import rmtree ENVIRONMENT_DIR = 'node_env' @@ -98,8 +100,33 @@ def install_environment( ) lang_base.setup_cmd(prefix, local_install_cmd) - _, pkg, _ = cmd_output('npm', 'pack', cwd=prefix.prefix_dir) - pkg = prefix.path(pkg.strip()) + npm_version = get_npm_version() + + args = ['npm', 'pack', '--json'] + # https://docs.npmjs.com/cli/v11/using-npm/changelog#1100-pre0-2024-11-26 + if npm_version >= (11, 0, 0): + args.append('--ignore-scripts') + + _, pkg, _ = cmd_output(*args, cwd=prefix.prefix_dir) + try: + pkg_json = json.loads(pkg) + except json.JSONDecodeError as e: + raise ValueError('Failed to parse npm pack output as JSON.') from e + + if not pkg_json: + raise ValueError('JSON array from npm pack is empty.') + + if not isinstance(pkg_json, list): + raise ValueError('Expected npm pack output to be a JSON array.') + + filename = pkg_json[0].get('filename') + if filename is None: + raise KeyError( + "Key 'filename' not found in the first element " + 'of the JSON array.', + ) + + pkg = prefix.path(filename) install = ('npm', 'install', '-g', pkg, *additional_dependencies) lang_base.setup_cmd(prefix, install) diff --git a/pre_commit/util.py b/pre_commit/util.py index 19b1880b..9b54e867 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -4,6 +4,7 @@ import contextlib import errno import importlib.resources import os.path +import re import shutil import stat import subprocess @@ -237,3 +238,13 @@ else: # pragma: >=3.12 cover def win_exe(s: str) -> str: return s if sys.platform != 'win32' else f'{s}.exe' + + +def get_npm_version() -> tuple[int, ...]: + _, out, _ = cmd_output('npm', '--version') + version_match = re.match(r'^(\d+)\.(\d+)\.(\d+)', out) + + if version_match is None: + return 0, 0, 0 + else: + return tuple(map(int, version_match.groups())) diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py index 055cb1e9..9dbc2a37 100644 --- a/tests/languages/node_test.py +++ b/tests/languages/node_test.py @@ -113,8 +113,8 @@ def test_installs_without_links_outside_env(tmpdir): assert cmd_output('foo')[1] == 'success!\n' -def _make_hello_world(tmp_path): - package_json = '''\ +def _make_hello_world(tmp_path, package_json=None): + package_json = package_json or '''\ {"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}} ''' tmp_path.joinpath('package.json').write_text(package_json) @@ -132,6 +132,20 @@ def test_node_hook_system(tmp_path): assert ret == (0, b'Hello World\n') +def test_node_with_prepare_script(tmp_path): + package_json = ''' +{ + "name": "t", + "version": "0.0.1", + "bin": {"node-hello": "./bin/main.js"}, + "scripts": {"prepare": "echo prepare"} +} +''' + _make_hello_world(tmp_path, package_json) + ret = run_language(tmp_path, node, 'node-hello') + assert ret == (0, b'Hello World\n') + + def test_node_with_user_config_set(tmp_path): cfg = tmp_path.joinpath('cfg') cfg.write_text('cache=/dne\n')