Skip to content

Commit

Permalink
feat(yarn_classic): implement 'Project' class
Browse files Browse the repository at this point in the history
## What

- a class representing a yarn project
- can create Projects from a source dir
- can check for (disallowed) PnP installs

## How

- implement `Project` as a frozen dataclass

Signed-off-by: Ben Alkov <[email protected]>
  • Loading branch information
ben-alkov committed Nov 13, 2024
1 parent 761d214 commit 9472027
Show file tree
Hide file tree
Showing 3 changed files with 121 additions and 2 deletions.
21 changes: 20 additions & 1 deletion cachi2/core/package_managers/yarn_classic/main.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging

from cachi2.core.errors import PackageManagerError
from cachi2.core.errors import PackageManagerError, PackageRejected
from cachi2.core.models.input import Request
from cachi2.core.models.output import Component, EnvironmentVariable, RequestOutput
from cachi2.core.package_managers.yarn.utils import (
VersionsRange,
extract_yarn_version_from_env,
run_yarn_cmd,
)
from cachi2.core.package_managers.yarn_classic.project import Project
from cachi2.core.package_managers.yarn_classic.workspaces import extract_workspace_metadata
from cachi2.core.rooted_path import RootedPath

Expand Down Expand Up @@ -91,6 +92,24 @@ def _generate_build_environment_variables() -> list[EnvironmentVariable]:
return [EnvironmentVariable(name=key, value=value) for key, value in env_vars.items()]


def _reject_if_pnp_install(project: Project) -> None:
if project.is_pnp_install:
raise PackageRejected(
reason=("Yarn PnP install detected; PnP installs are unsupported by cachi2"),
solution=(
"Please convert your project to a regular install-based one.\n"
"If you use Yarn's PnP, please remove `installConfig.pnp: true`"
" from 'package.json', any file(s) with glob name '*.pnp.cjs',"
" and any 'node_modules' directories."
),
)


def _verify_repository(project: Project) -> None:
_reject_if_pnp_install(project)
# _check_lockfile(project)


def _verify_corepack_yarn_version(source_dir: RootedPath, env: dict[str, str]) -> None:
"""Verify that corepack installed the correct version of yarn by checking `yarn --version`."""
installed_yarn_version = extract_yarn_version_from_env(source_dir, env)
Expand Down
29 changes: 29 additions & 0 deletions cachi2/core/package_managers/yarn_classic/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,32 @@ def from_file(cls, path: RootedPath) -> "PackageJson":


ConfigFile = Union[PackageJson]


@dataclass(frozen=True)
class Project:
"""Minimally, a directory containing yarn sources and parsed package.json."""

source_dir: RootedPath
package_json: PackageJson

@property
def is_pnp_install(self) -> bool:
"""Is the project is using Plug'n'Play (PnP) workflow or not.
This is determined by
- `installConfig.pnp: true` in 'package.json'
- the existence of file(s) with glob name '*.pnp.cjs'
- the presence of an expanded node_modules directory
For more details on PnP, see: https://classic.yarnpkg.com/en/docs/pnp
"""
install_config_pnp_enabled = self.package_json.install_config.get("pnp", False)
pnp_cjs_exists = any(self.source_dir.path.glob("*.pnp.cjs"))
node_modules_exists = self.source_dir.join_within_root("node_modules").path.exists()
return install_config_pnp_enabled or pnp_cjs_exists or node_modules_exists

@classmethod
def from_source_dir(cls, source_dir: RootedPath) -> "Project":
"""Create a Project from a sources directory path."""
package_json = PackageJson.from_file(source_dir.join_within_root("package.json"))
return cls(source_dir, package_json)
73 changes: 72 additions & 1 deletion tests/unit/package_managers/yarn_classic/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import pytest

from cachi2.core.errors import PackageRejected
from cachi2.core.package_managers.yarn_classic.project import ConfigFile, PackageJson
from cachi2.core.package_managers.yarn_classic.main import _verify_repository
from cachi2.core.package_managers.yarn_classic.project import ConfigFile, PackageJson, Project
from cachi2.core.rooted_path import RootedPath

VALID_PACKAGE_JSON_FILE = """
Expand Down Expand Up @@ -37,6 +38,13 @@ def _prepare_config_file(
return config_file_class.from_file(path)


def _setup_pnp_installs(rooted_tmp_path: RootedPath, pnp_kind: str) -> None:
if pnp_kind == "node":
rooted_tmp_path.join_within_root("node_modules").path.mkdir()
if pnp_kind == "pnp_cjs":
rooted_tmp_path.join_within_root("foo.pnp.cjs").path.touch()


@pytest.mark.parametrize(
"config_file_class, config_file_name, config_file_content, config_kind",
[
Expand Down Expand Up @@ -132,3 +140,66 @@ def test_from_file_missing(
with pytest.raises(PackageRejected):
path = rooted_tmp_path.join_within_root(config_file_name)
config_file_class.from_file(path)


@pytest.mark.parametrize(
"config_file_class, config_file_name, config_file_content, pnp_kind",
[
pytest.param(
PackageJson,
"package.json",
PNP_PACKAGE_JSON_FILE,
"install_config",
id="installConfig",
),
pytest.param(PackageJson, "package.json", VALID_PACKAGE_JSON_FILE, "node", id="node"),
pytest.param(PackageJson, "package.json", VALID_PACKAGE_JSON_FILE, "pnp_cjs", id="pnp_cjs"),
],
)
def test_pnp_installs_true(
rooted_tmp_path: RootedPath,
config_file_class: ConfigFile,
config_file_name: str,
config_file_content: str,
pnp_kind: str,
) -> None:
_prepare_config_file(
rooted_tmp_path,
config_file_class,
config_file_name,
config_file_content,
)

project = Project.from_source_dir(rooted_tmp_path)

_setup_pnp_installs(rooted_tmp_path, pnp_kind)
with pytest.raises(PackageRejected):
_verify_repository(project)


@pytest.mark.parametrize(
"config_file_class, config_file_name, config_file_content",
[
pytest.param(
PackageJson,
"package.json",
VALID_PACKAGE_JSON_FILE,
),
],
)
def test_pnp_installs_false(
rooted_tmp_path: RootedPath,
config_file_class: ConfigFile,
config_file_name: str,
config_file_content: str,
) -> None:
_prepare_config_file(
rooted_tmp_path,
config_file_class,
config_file_name,
config_file_content,
)

project = Project.from_source_dir(rooted_tmp_path)

_verify_repository(project)

0 comments on commit 9472027

Please sign in to comment.