gruntjs / gruntjs/grunt

loadNpmTasks fails to find task plugins when working with monorepo symlinks

Open
#1,732 2 comments 0 reactions 0 assignees View on GitHub
Type: Enhancement
Dominant language
JavaScript
Stars
12.2k
Forks
1.5k
PR merge metrics
No merged PRs in 30d

Description

When a dependency has been symlinked into a monorepo application, and this dependency is hoisted to the monorepo root, the dependency's `tasks/` folder cannot be located by `grunt.loadNpmTasks` due to an incorrect assumption made when determining the root filepath.

The core issue is that the `name` parameter that `loadNpmTasks` is invoked with is not required to be a valid path part due to symlinks. Consider the example directory layout:
```
dev/
corp-utils-monorepo/
applications/
neato-task-plugins/
tasks/
cool-task.js
package.json
// other stuff from the company
my-team-monorepo/
packages/
team-app-1/
Gruntfile.js
package.json
// other monorepo projects that also use the neato-task-plugins package
```

As a Good Company, scopes are used to guard internal packages from dependency squatting, and thus `neato-task-plugins` is published internally with the `@mycorp` scope. Its tasks are loaded from the `team-app-1` package Gruntfile with `grunt.loadNpmTasks('@mycorp/neato-task-plugins');`

All is well, until one tries to develop a new corp-wide task and symlinks the task package by running e.g. `yarn link '@mycorp/neato-task-plugins'` from the `my-team-monorepo` directory.

Now, running the previously working grunt tasks fail with an error logged from here:
https://github.com/gruntjs/grunt/blob/ee722d15ed214c824d2925d04afef10f217338c3/lib/grunt/task.js#L412-L417
e.g. `>> Local Npm module "@mycorp/neato-task-plugins" not found. Is it installed?`

And caused by the setting of `root` here:
https://github.com/gruntjs/grunt/blob/ee722d15ed214c824d2925d04afef10f217338c3/lib/grunt/task.js#L384

Where the variables have values like:
```js
pkgfile === '/Users/me/dev/corp-utils-monorepo/applications/neato-task-plugins/package.json';
normailzedName === '@mycorp/neato-task-plugins';
// root === '/Users/me/dev/corp-utils-monorepo/appli'
```

However, we even after we fix the calculation of `root`, e.g. by doing something like slicing off 2 (or 3 for scoped packages) path elements, the code still fails to compute `tasksdir` correctly, looking for
`/Users/me/dev/corp-utils-monorepo/applications/@mycorp/neato-task-plugins/tasks` instead of
`/Users/me/dev/corp-utils-monorepo/applications/neato-task-plugins/tasks`.

I think the best solution is to use relative paths from the resolved `pkgfile`, rather than attempt to use `path.join` with varied arguments of `root`, `name`, or `node_modules`:
```js
var tasksdir = path.join(path.dirname(pkgfile), 'tasks');
```

I'm not sure what the behavior should be for "collection plugins" that are symlinked, as I'm not familiar with those.

Contributor guide

Open the contributing guide

Research direction

Start in lib/grunt/task.js around the root calculation at lines 384 and the missing-module error at lines 412-417. Reproduce loading a scoped, symlinked plugin and trace the resolved pkgfile; done means its tasks/ directory is found without breaking collection-plugin behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, node.js
Domain
tooling
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.