Skip to content

Commit dd675aa

Browse files
Improve npm workspace processing (#3857)
Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent e26187a commit dd675aa

File tree

15 files changed

+9715
-7180
lines changed

15 files changed

+9715
-7180
lines changed

src/packagedcode/npm.py

Lines changed: 120 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ def logger_debug(*args):
6969

7070
class BaseNpmHandler(models.DatafileHandler):
7171

72+
lockfile_names = {
73+
'package-lock.json',
74+
'.package-lock.json',
75+
'npm-shrinkwrap.json',
76+
'yarn.lock',
77+
'shrinkwrap.yaml',
78+
'pnpm-lock.yaml'
79+
}
80+
7281
@classmethod
7382
def assemble(cls, package_data, resource, codebase, package_adder):
7483
"""
@@ -85,19 +94,11 @@ def assemble(cls, package_data, resource, codebase, package_adder):
8594
If there is no package.json, we do not have a package instance. In this
8695
case, we yield each of the dependencies in each lock file.
8796
"""
88-
lockfile_names = {
89-
'package-lock.json',
90-
'.package-lock.json',
91-
'npm-shrinkwrap.json',
92-
'yarn.lock',
93-
'shrinkwrap.yaml',
94-
'pnpm-lock.yaml'
95-
}
9697

9798
package_resource = None
9899
if resource.name == 'package.json':
99100
package_resource = resource
100-
elif resource.name in lockfile_names:
101+
elif resource.name in cls.lockfile_names:
101102
if resource.has_parent():
102103
siblings = resource.siblings(codebase)
103104
package_resource = [r for r in siblings if r.name == 'package.json']
@@ -117,10 +118,15 @@ def assemble(cls, package_data, resource, codebase, package_adder):
117118
pkg_data = package_resource.package_data[0]
118119
pkg_data = models.PackageData.from_dict(pkg_data)
119120

120-
workspace_root_path = package_resource.parent(codebase).path
121+
workspace_root = package_resource.parent(codebase)
122+
workspace_root_path = None
123+
if workspace_root:
124+
workspace_root_path = package_resource.parent(codebase).path
121125
workspaces = pkg_data.extra_data.get('workspaces') or []
126+
122127
# Also look for pnpm workspaces
123-
if not workspaces:
128+
pnpm_workspace = None
129+
if not workspaces and workspace_root:
124130
pnpm_workspace_path = os.path.join(workspace_root_path, 'pnpm-workspace.yaml')
125131
pnpm_workspace = codebase.get_resource(path=pnpm_workspace_path)
126132
if pnpm_workspace:
@@ -139,7 +145,7 @@ def assemble(cls, package_data, resource, codebase, package_adder):
139145
cls.update_workspace_members(workspace_members, codebase)
140146

141147
# do we have enough to create a package?
142-
if pkg_data.purl:
148+
if pkg_data.purl and not workspaces:
143149
package = models.Package.from_package_data(
144150
package_data=pkg_data,
145151
datafile_path=package_resource.path,
@@ -151,35 +157,128 @@ def assemble(cls, package_data, resource, codebase, package_adder):
151157
# Always yield the package resource in all cases and first!
152158
yield package
153159

154-
root = package_resource.parent(codebase)
155-
if root:
156-
for npm_res in cls.walk_npm(resource=root, codebase=codebase):
160+
if workspace_root:
161+
for npm_res in cls.walk_npm(resource=workspace_root, codebase=codebase):
157162
if package_uid and package_uid not in npm_res.for_packages:
158163
package_adder(package_uid, npm_res, codebase)
159164
yield npm_res
160-
elif codebase.has_single_resource:
161-
if package_uid and package_uid not in package_resource.for_packages:
162-
package_adder(package_uid, package_resource, codebase)
163165
yield package_resource
164166

167+
elif workspaces:
168+
yield from cls.create_packages_from_workspaces(
169+
workspace_members=workspace_members,
170+
workspace_root=workspace_root,
171+
codebase=codebase,
172+
package_adder=package_adder,
173+
pnpm=pnpm_workspace and pkg_data.purl,
174+
)
175+
176+
package_uid = None
177+
if pnpm_workspace and pkg_data.purl:
178+
package = models.Package.from_package_data(
179+
package_data=pkg_data,
180+
datafile_path=package_resource.path,
181+
)
182+
package_uid = package.package_uid
183+
184+
package.populate_license_fields()
185+
186+
# Always yield the package resource in all cases and first!
187+
yield package
188+
189+
if workspace_root:
190+
for npm_res in cls.walk_npm(resource=workspace_root, codebase=codebase):
191+
if package_uid and not npm_res.for_packages:
192+
package_adder(package_uid, npm_res, codebase)
193+
yield npm_res
194+
yield package_resource
195+
165196
else:
166197
# we have no package, so deps are not for a specific package uid
167198
package_uid = None
168199

200+
yield from cls.yield_npm_dependencies_and_resources(
201+
package_resource=package_resource,
202+
package_data=pkg_data,
203+
package_uid=package_uid,
204+
codebase=codebase,
205+
package_adder=package_adder,
206+
)
207+
208+
@classmethod
209+
def yield_npm_dependencies_and_resources(cls, package_resource, package_data, package_uid, codebase, package_adder):
210+
169211
# in all cases yield possible dependencies
170-
yield from yield_dependencies_from_package_data(pkg_data, package_resource.path, package_uid)
212+
yield from yield_dependencies_from_package_data(package_data, package_resource.path, package_uid)
171213

172214
# we yield this as we do not want this further processed
173215
yield package_resource
174216

175217
for lock_file in package_resource.siblings(codebase):
176-
if lock_file.name in lockfile_names:
218+
if lock_file.name in cls.lockfile_names:
177219
yield from yield_dependencies_from_package_resource(lock_file, package_uid)
178220

179221
if package_uid and package_uid not in lock_file.for_packages:
180222
package_adder(package_uid, lock_file, codebase)
181223
yield lock_file
182224

225+
@classmethod
226+
def create_packages_from_workspaces(
227+
cls,
228+
workspace_members,
229+
workspace_root,
230+
codebase,
231+
package_adder,
232+
pnpm=False,
233+
):
234+
235+
workspace_package_uids = []
236+
for workspace_member in workspace_members:
237+
if not workspace_member.package_data:
238+
continue
239+
240+
pkg_data = workspace_member.package_data[0]
241+
pkg_data = models.PackageData.from_dict(pkg_data)
242+
243+
package = models.Package.from_package_data(
244+
package_data=pkg_data,
245+
datafile_path=workspace_member.path,
246+
)
247+
package_uid = package.package_uid
248+
workspace_package_uids.append(package_uid)
249+
250+
package.populate_license_fields()
251+
252+
# Always yield the package resource in all cases and first!
253+
yield package
254+
255+
member_root = workspace_member.parent(codebase)
256+
package_adder(package_uid, member_root, codebase)
257+
for npm_res in cls.walk_npm(resource=member_root, codebase=codebase):
258+
if package_uid and package_uid not in npm_res.for_packages:
259+
package_adder(package_uid, npm_res, codebase)
260+
yield npm_res
261+
262+
yield from cls.yield_npm_dependencies_and_resources(
263+
package_resource=workspace_member,
264+
package_data=pkg_data,
265+
package_uid=package_uid,
266+
codebase=codebase,
267+
package_adder=package_adder,
268+
)
269+
270+
# All resources which are not part of a workspace package exclusively
271+
# are a part of all packages (this is skipped if we have a root pnpm
272+
# package)
273+
if pnpm:
274+
return
275+
for npm_res in cls.walk_npm(resource=workspace_root, codebase=codebase):
276+
if npm_res.for_packages:
277+
continue
278+
279+
npm_res.for_packages = workspace_package_uids
280+
npm_res.save(codebase)
281+
183282
@classmethod
184283
def walk_npm(cls, resource, codebase, depth=0):
185284
"""
@@ -1081,6 +1180,7 @@ def parse(cls, location, package_only=False):
10811180
name, version = name_version.split("@")
10821181
elif major_v == "5" or is_shrinkwrap:
10831182
if len(sections) == 3:
1183+
namespace = None
10841184
_, name, version = sections
10851185
elif len(sections) == 4:
10861186
_, namespace, name, version = sections

tests/packagedcode/data/npm/pnpm/pnpm-lock/v5/cobe-pnpm-lock.yaml-expected

Lines changed: 21 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)