99from hermeto .core .models .input import Request
1010from hermeto .core .models .output import Component , EnvironmentVariable , RequestOutput
1111from hermeto .core .models .sbom import create_backend_annotation
12+ from hermeto .core .package_managers .yarn .locators import WorkspaceLocator
1213from hermeto .core .package_managers .yarn .project import (
14+ PackageJson ,
1315 Plugin ,
1416 Project ,
1517 YarnRc ,
1618 get_semver_from_package_manager ,
1719 get_semver_from_yarn_path ,
1820)
19- from hermeto .core .package_managers .yarn .resolver import create_components , resolve_packages
21+ from hermeto .core .package_managers .yarn .resolver import (
22+ Package ,
23+ create_components ,
24+ resolve_packages ,
25+ )
2026from hermeto .core .package_managers .yarn .utils import (
2127 VersionsRange ,
2228 extract_yarn_version_from_env ,
@@ -35,7 +41,7 @@ def fetch_yarn_source(request: Request) -> RequestOutput:
3541 path = request .source_dir .join_within_root (package .path )
3642 project = Project .from_source_dir (path )
3743
38- components .extend (_resolve_yarn_project (project , request .output_dir ))
44+ components .extend (_resolve_yarn_project (project , request .output_dir , package . workspaces ))
3945
4046 annotations = []
4147 if backend_annotation := create_backend_annotation (components , "yarn" ):
@@ -101,21 +107,38 @@ def _verify_repository(project: Project) -> None:
101107 _check_lockfile (project )
102108
103109
104- def _resolve_yarn_project (project : Project , output_dir : RootedPath ) -> list [Component ]:
110+ def _resolve_yarn_project (
111+ project : Project ,
112+ output_dir : RootedPath ,
113+ workspaces : list [str ] | None = None ,
114+ ) -> list [Component ]:
105115 """Process a request for a single yarn source directory.
106116
107117 :param project: the directory to be processed.
108118 :param output_dir: the directory where the prefetched dependencies will be placed.
119+ :param workspaces: optional list of workspace names to focus on (Yarn v4 only).
109120 :raises PackageManagerError: if fetching dependencies fails
110121 """
111122 log .info (f"Fetching the yarn dependencies at the subpath { project .source_dir } " )
112123
113124 version = _configure_yarn_version (project )
125+
126+ if workspaces and version < semver .Version .parse ("4.0.0" ):
127+ raise PackageRejected (
128+ f"Workspace focus requires Yarn v4 or later, but this project uses Yarn { version } " ,
129+ solution = "Either upgrade to Yarn v4 or remove the 'workspaces' field from the input." ,
130+ )
131+
114132 _verify_repository (project )
115133
116134 _set_yarnrc_configuration (project , output_dir , version )
117- packages = resolve_packages (project .source_dir )
118- _fetch_dependencies (project .source_dir )
135+
136+ packages = resolve_packages (project .source_dir , workspaces )
137+
138+ if workspaces :
139+ _strip_workspace_scripts (project .source_dir , packages )
140+
141+ _fetch_dependencies (project .source_dir , workspaces )
119142
120143 return create_components (packages , project , output_dir )
121144
@@ -241,14 +264,44 @@ def _set_yarnrc_configuration(
241264 yarn_rc .write ()
242265
243266
244- def _fetch_dependencies (source_dir : RootedPath ) -> None :
245- """Fetch dependencies using 'yarn install'.
267+ def _strip_workspace_scripts (source_dir : RootedPath , packages : list [Package ]) -> None :
268+ """Remove scripts from workspace package.json files.
269+
270+ yarn workspaces focus does not support --mode skip-build, and enableScripts: false
271+ does not apply to workspace scripts (https://github.com/yarnpkg/berry/pull/4781).
272+ Stripping the scripts field prevents lifecycle scripts from executing during focus.
273+
274+ :param source_dir: the project source directory.
275+ :param packages: packages returned by ``resolve_packages``, used to find workspace paths.
276+ """
277+ for pkg in packages :
278+ locator = pkg .parsed_locator
279+ if not isinstance (locator , WorkspaceLocator ):
280+ continue
281+ pkg_json_path = source_dir .join_within_root (locator .relpath , "package.json" )
282+ if not pkg_json_path .path .exists ():
283+ continue
284+ pkg_json = PackageJson .from_file (pkg_json_path )
285+ if "scripts" in pkg_json :
286+ del pkg_json ["scripts" ]
287+ pkg_json .write ()
288+
289+
290+ def _fetch_dependencies (source_dir : RootedPath , workspaces : list [str ] | None = None ) -> None :
291+ """Fetch dependencies using 'yarn install' or 'yarn workspaces focus'.
292+
293+ When workspaces are specified, only the dependencies of those workspaces (and their
294+ transitive workspace dependencies) are installed via 'yarn workspaces focus'.
246295
247296 :param source_dir: the directory in which the yarn command will be called.
248- :raises PackageManagerError: if the 'yarn install' command fails.
297+ :param workspaces: optional list of workspace names to focus on (Yarn v4 only).
298+ :raises PackageManagerError: if the yarn command fails.
249299 """
250300 try :
251- run_yarn_cmd (["install" , "--mode" , "skip-build" ], source_dir )
301+ if workspaces :
302+ run_yarn_cmd (["workspaces" , "focus" , * workspaces ], source_dir )
303+ else :
304+ run_yarn_cmd (["install" , "--mode" , "skip-build" ], source_dir )
252305 except PackageManagerError as e :
253306 # TODO: this follows a precedent set in resolver. Either a more robust way for
254307 # dealing with this must be found or a comment provided that such methods do not exist.
0 commit comments