google/tachometer

Discover local dependencies outside of immediate node_modules

nolanlawson opened this issue · 2 comments

I really hope that I'm just missing something obvious, so please close this issue if I just misread the documentation or something.

Let's say I have a very simple module with a single index.js that I want to test. If I have this in my tachometer.json:

{
  "benchmarks": [
    {
      "url": "./benchmark.html",
      "expand": [
        {
          "name": "this-change"
        },
        {
          "name": "tip-of-tree",
          "packageVersions": {
            "label": "tip-of-tree",
            "dependencies": {
              "my-package-name": {
                "kind": "git",
                "repo": "https://github.com/org/my-package-name.git",
                "ref": "master"
              }
            }
          }
        }
      ]
    }
  ]
}

And then I use this in my benchmark.html:

import myPackage from 'my-package-name'

This actually works for the remote dependency, but it doesn't work for the local dependency. Instead, Tachometer doesn't transform the import statement, so the browser can't find the module.

Screenshot from 2021-07-02 14-54-52

Here is a small repro.

The only solution I've found is to manually add symlinks so that node_modules/my-package-name is linked locally:

ln -s .. node_modules/repro-tach-dependency

Then Tachometer works as expected - the local dependency is resolved locally, and the remote one is resolved by fetching it from npm.

I've also observed this issue in a monorepo, where the project structure is like this:

packages/my-package-name
packages/my-package-name/index.js
packages/benchmark
packages/benchmark/benchmark.html

In this case, if benchmark.html tries to do import 'my-package-name', it won't resolve locally.

This might have something to do with subtleties of dependency hoisting. In a monorepo project without dependency hoisting, packages/benchmark/node_modules would indeed contain the my-package-name package. But in the case I ran into, dependency hoisting moves all the dependencies to the top-level node_modules, so Tachometer can't find it, because it seems to check only the immediate node_modules directory.

The solution I've found is again to do the node_modules symlinking, e.g.:

ln -s ../../my-package-name ./packages/benchmark/node_modules/my-package-name

To solve both these issues (if this is indeed an issue and I didn't just misconfigure something), it seems to me that the Tachometer module resolver should:

  • search for packages at the root level via package.json (for the simple single-package case)
  • search recursively up the tree for node_modules (for the hoisted monorepo case)

Thanks for reading this far, and thanks for creating Tachometer! It's a great tool, and I can use it just fine with the symlinking workaround, but I thought I'd report the issue in case others ran into it or I missed something. 🙂

After thinking about this more deeply, I think this can be solved with:

  • root (as described here: #244 (comment))
  • not referring to your own package as a bare 'my-package-name' but instead as e.g. './index.js'

I'm not sure why the resolution works differently for remote vs local tests, but in any case, Tachometer is probably right that it would be weird to resolve 'my-package-name' to the current directory when running inside the my-package-name directory itself. For example, this doesn't work in Node:

mkdir foo
cd foo
npm init --yes
node -e "require('foo')" # Error: Cannot find module 'foo'

So since the resolution doesn't work in plain Node, you wouldn't really expect it to work in Tachometer either. So referring to your own local package as e.g. './index.js' makes a lot more sense.

As for the monorepo issue, I think this is the same as #244 and can be resolved with root. Closing this issue. Sorry for the noise!

For the record, this issue was actually trickier to resolve than I thought. I'll put a summary here in case it helps anyone else.

Situation: you have a package, e.g. my-package, and you want to test the local branch versus a remote (e.g. main). There is no monorepo; you want to test a single package and have Tachometer swap it out.

Resolution: use a placeholder package in your package.json:

"devDependencies": {
  "@scope/placeholder": "file:."
}

Then import from that placeholder in your benchmark scripts:

import foo from '@scope/placeholder'

Then tell Tachometer to swap out the placeholder package when resolving the main branch dependency:

  "expand": [
    {
      "name": "this-change"
    },
    {
      "name": "tip-of-tree",
      "packageVersions": {
        "label": "tip-of-tree",
        "dependencies": {
          "@scope/placeholder": {
            "kind": "git",
            "repo": "https://github.com/scope/package.git",
            "ref": "main"
          }
        }
      }
    }
  ]

It's a little gnarly, but it works! Note the @scope to avoid accidental dependency confusion attacks.