Skip to content

Commit

Permalink
Add a node_scope option to node_module targets to support package-sco…
Browse files Browse the repository at this point in the history
…pes (pantsbuild#6616)

This is the implementation of pantsbuild#6540.

### Problem
Pants source-level dependencies for Node.js targets does not support [node-scopes](https://docs.npmjs.com/misc/scope).

### Solution
Add a `node_scope` config to `node_module` targets and a repo-level `node_scope` to `node-distribution`.

### Result
The repo-owner may specify a target-level and/or global node-scope that will resolve all Node.js source-level dependencies under the correct node-scope rules.

For example, if you have a target dependency on **src/node/pants/custom/web-package:web-package** with a package name of `web-package`, the installed path under the `node_modules` directory would be `*/node_modules/web-package`.

If a node_scope=pants is defined for `src/node/pants/custom/web-package:web-package`. Then the installed directory would be `*/node_modules/@pants/web-package`.

Target-level `node_scope` overrides repo-level `node_scope`.

### Defining scope in package.json

When dep inference is ready, a potential update to this implementation is to be able to infer the node_scope from the name defined in package.json.
  • Loading branch information
nsaechao authored and baroquebobcat committed Oct 15, 2018
1 parent aa656d6 commit 31f4000
Show file tree
Hide file tree
Showing 4 changed files with 281 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ def register_options(cls, register):
help='The path to the global eslint ignore path')
register('--eslint-version', default='4.15.0', fingerprint=True,
help='Use this ESLint version.')
register('--node-scope', advanced=True, fingerprint=True,
help='Default node scope for repo. Scope groups related packages together.')

@memoized_method
def _get_package_managers(self):
Expand Down Expand Up @@ -116,6 +118,10 @@ def eslint_config(self):
def eslint_ignore(self):
return self.get_options().eslint_ignore

@memoized_property
def node_scope(self):
return self.get_options().node_scope

@memoized_method
def _install_node(self):
"""Install the Node distribution from pants support binaries.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,23 +134,49 @@ def resolve_target(self, node_task, target, results_dir, node_paths, resolve_loc
raise TaskError('Failed to resolve dependencies for {}:\n\t{} failed with exit code {}'
.format(target.address.reference(), command, result))
if source_deps:
self._link_source_dependencies(target, results_dir, node_paths, source_deps)

def _link_source_dependencies(self, target, results_dir, node_paths, source_deps):
self._link_source_dependencies(node_task, target, results_dir, node_paths, source_deps)

def _link_source_dependencies(self, node_task, target, results_dir, node_paths, source_deps):
for package_name, file_path in source_deps.items():
# Package name should always the same as the target name
dep = self._get_target_from_package_name(target, package_name, file_path)

# Apply node-scoping rules if applicable
node_scope = dep.payload.node_scope or node_task.node_distribution.node_scope
dep_package_name = self._scoped_package_name(node_task, dep.package_name, node_scope)
# Symlink each target
dep_path = node_paths.node_path(dep)
node_module_dir = os.path.join(results_dir, 'node_modules')
relative_symlink(dep_path, os.path.join(node_module_dir, dep.package_name))
relative_symlink(dep_path, os.path.join(node_module_dir, dep_package_name))
# If there are any bin, we need to symlink those as well
bin_dir = os.path.join(node_module_dir, '.bin')
for bin_name, rel_bin_path in dep.bin_executables.items():
bin_path = os.path.join(dep_path, rel_bin_path)
relative_symlink(bin_path, os.path.join(bin_dir, bin_name))

@staticmethod
def _scoped_package_name(node_task, package_name, node_scope):
"""Apply a node_scope to the package name.
Overrides any existing package_name if already in a scope
:return: A package_name with prepended with a node scope via '@'
"""

if not node_scope:
return package_name

scoped_package_name = package_name
chunk = package_name.split('/', 1)
if len(chunk) > 1 and chunk[0].startswith('@'):
scoped_package_name = os.path.join('@{}'.format(node_scope), chunk[1:])
else:
scoped_package_name = os.path.join('@{}'.format(node_scope), package_name)

node_task.context.log.debug(
'Node package "{}" will be resolved with scope "{}".'.format(package_name, scoped_package_name))
return scoped_package_name

@staticmethod
def _emit_package_descriptor(node_task, target, results_dir, node_paths):
dependencies = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ class NodeModule(NodePackage):

def __init__(
self, package_manager=None, sources=None, build_script=None, output_dir='dist',
dev_dependency=False, style_ignore_path='.eslintignore', address=None, payload=None,
bin_executables=None, **kwargs):
dev_dependency=False, style_ignore_path='.eslintignore', address=None, payload=None,
bin_executables=None, node_scope=None, **kwargs):
"""
:param sources: Javascript and other source code files that make up this module; paths are
relative to the BUILD file's directory.
Expand All @@ -49,6 +49,11 @@ def __init__(
E.G. { 'app': './cli.js', 'runner': './scripts/run.sh' }
`string`, file path and package name as the default bin name
E.G. './cli.js' would be interpreted as { 'app': './cli.js' }
:param node_scope: Groups related packages together by adding a scope. The `@`
symbol is typically used for specifying scope in the package name in `package.json`.
However pants target addresses do not allow for `@` in the target address.
A repo-level default scope can be added with the --node-distribution-node-scope option.
Any target-level node_scope will override the global node-scope.
"""
# TODO(John Sirois): Support devDependencies, etc. The devDependencies case is not
Expand All @@ -74,6 +79,7 @@ def __init__(
'dev_dependency': PrimitiveField(dev_dependency),
'style_ignore_path': PrimitiveField(style_ignore_path),
'bin_executables': PrimitiveField(bin_executables),
'node_scope': PrimitiveField(node_scope),
})
super(NodeModule, self).__init__(address=address, payload=payload, **kwargs)

Expand Down
Loading

0 comments on commit 31f4000

Please sign in to comment.