New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Find named exports #365

Merged
merged 108 commits into from Jan 31, 2017

Conversation

Projects
None yet
2 participants
@trotzig
Collaborator

trotzig commented Jan 20, 2017

This ended up taking a little longer than I was hoping for. I've been slowly iterating from a version of import-js that traverses the file system on every import into a version that keeps an up-to-date cache/index of modules. I'm finally in a stable (ish) place. 馃槂

I don't expect anyone to look at all these changes. I've published a beta version (2.1.0-beta.5) that you can try out.

The most important files added/changed are:

  • ModuleFinder
  • ExportsStorage
  • findExports

Ping @rhettlivingston, @lencioni - if either of you have time to review, I'd be thrilled. And @rhettlivingston, since this is a complete overhaul of named exports, it's likely that I might have broken something in the meteor environment. It could also be that you are able to make use of the new system to make finding named meteor packages simpler.

trotzig added some commits Dec 25, 2016

Use `declarationKeyword: 'import'` locally
With version 2.0 of import-js, node projects now get `const` as the
default declarationKeyword. We use babel here, so we don't want the
default.
Add function to find exports from file
I'm about to continue building off of this, keeping an up-to-date index
of all exports.

This is a naive first implementation. I'm hoping this can get us a good
deal on the way without actually running the code in the files.
Keep up-to-date index of files and exports
This is still in the early stages, but the idea seems to work: Listen to
watchman changes to files, parse them for named exports, and store an
index of all named exports. Importing then becomes a quick check in this
index.

For now, I've kept the old file-scanning finders intact. I've opened up
a discussion around discontinuing these:
#357
Move caching of exports to new class
I'm about to replace the in-memory cache with something persistent.
Before I do that, I wanted to clean up the API and extract something
with a single responsibility.
Move name normalizing to ExportsCache.js
It makes more sense for normalization to happen here.
Switch to sqlite for exports cache
This is no longer just a cache, so I plan on renaming the module soon.

Instead of recomputing named exports for a file every single time we
start importjs, we can keep a filesystem cache of exports. I choose
sqlite here because

 - I'm comfortable with SQL
 - It will handle concurrency

On each detected file change, we first check the modification time of
the file to decide whether to update the cache/db or not.
Fix checking thousands of file in `needsUpdate()`
I ran into an issue when trying the new sqlite backed exports cache on
the brigade codebase. It has 1400+ javascript file, and with that many
files to check, the query to check mtimes ended up having too many
arguments and sqlite threw an error. I fixed this by batching the query
if needed.
Remove old exports as part of updating
If a file removed an export at some point, it would never be deleted
from the db. Now we remove all old ones before updating entries for a
file.
Grab mtime from watchman instead of filesystem
I forgot that we can specify fields we are interested in from watchman.
By doing that we no longer need to lstat files.
Add .importjs.db to .gitignore
We don't want to commit this file which is used as a cache by import-js
Simplify creation of WatchmanFileCache
Now that it's controlled by ModuleFinder, we don't need the factory
method.

Also removed watchman from a test where we don't rely on it any longer.
Remove benchmark script
With the new ModuleFinder, we don't need to benchmark different finders
against each other. We probably still need benchmarking, but this is not
the one we want.
Rename WatchmanFileCache => Watcher
This is no longer a cache. I've been meaning to rename it for a while,
but I hadn't come up with a good name until now.
Rename ExportsCache => ExportsStorage
This is no longer just a cache.
Switch regular file finder over to ModuleFinder as well
In the past, we've had a few file finders available, each with
different capabilities:

 - the "find" finder - fast but mac/linux only
 - the "node" finder - cross-platform but slightly slower
 - the "watchman" finder - depending on watchman running

I've moved over watchman to ModuleFinder, and until now kept the old
ones intact. That's changing now however. On each import, we will
`ensureUpToDate()` on ModuleFinder. If the watchman watcher isn't
running, we fallback to a full filesystem scan followed by an update to
storage. This way we only have one source of truth (the db).

I had to bring back the lastUpdate module to get modification times for
files.
Fix updating mtimes in sqlite db
We weren't passing in the right values to the SQL. I noticed this when
the same 5 files kept popping up as changed.
Also strip out dots when normalizing export names
This is because sometimes modules will have file endings baked into
them. Consider this module

./foo/Bar.js/package.json

We then want to be able to use BarJS as the variable name.
Use polling by default
Sorry for the mixed-up concerns in this commit. I started out wanting to
unify module finding between watchman and non-watchman, but ended up
fixing most Importer specs as well.

Highlights:
- switch to a list of default export names, so that we can match
./react/addons.js to a variable named "ReactAddons".
- The Watcher class will now fall back to a polling-based watch if
watchman is unavailable.
- Simplified the interface with Watcher by removing the listeners
pattern and replacing with onFilesAdded/onFilesRemoved options passed in
to the constructor.
Remove Importer tests for `excludes`
Since we're mocking out the file system here, there's no way we can
reliably test that the excludes config works. A better place for this
test would be in a Watcher test.
Fix flow errors
One of the problems was a real one, where I had forgot to initialize a
variable.
Remove files from mtimes table as well
I had forgot to remove things from the mtimes table when a file was
deleted. This resulted in no files ever being fully evicted from
storage.
Fix removing files when polling
We needed to reuse the storage object initialized in ModuleFinder.
Fix ExportStorage test
I forgot to update this when I switched defaultName to an array.
Don't normalize named exports
We don't want an export named e.g. _internal to match a variable named
"Internal". To make this happen, I changed the data model a little to
not store both key and name for an export. The key was the normalized
version of the name. We can instead store the raw export names for named
exports, and normalized names for default exports (since these are
constructed using the file path).
Fix not finding used variables referenced in flow
I came across this while fixing imports for lib/Watcher.js which has a
reference to ExportStorage in a flow annotation (and nowhere else).
Simplify folder matching to only include one level
Before, we would try to match variables over multiple/paths/to/file.js.
We don't really need that, or at least we can't think of the usecase
that led us to do this. Matching one level deep is enough based on a
discussion @lencioni and I had.
Remove unnecessary space before flow colon
Travis was yelling about this. My local version of flow did not alert me
about it.
Move exports db to tmp folder
This will hide implementation details to the user. It might still be
worthwile to know where the cache file is located. For that purpose,
`importjs cache` exists, which will output the location of the cache for
the current working dir.
Don't assume default exports
If a module doesn't have a `export default` or a `module.exports`, we
assume that it can't be imported.

I'm not 100% sure about this being the right move. Perhaps we should
keep assuming default exports after all. I want to give this a try first
though.
Fix findUndefinedIdentifiers for certain object references
My naive assumption about keys holding enough information to determine
whether an Identifier node is an assigment or a reference has proven to
be too naive. I found an example where we wouldn't find `uuid` as a
undefined variable in this setup:

    foo({
      bar() {},
      baz: 12,
      uuid: uuid.v4(),
    });

By passing in a context containing parent nodes (simplified) to the
visitor, we can better determine what type of identifier we're dealing
with.

I'm considering bringing in something like babel-traverse to simplify
traversing over the ast nodes. It would probably make the code more
robust. At this time however, I'm still learning the whole process of
traversing over nodes. I'd like to get some more experience on that
before I decide if it's worth bringing in a heavy-weight tool for the
job.
Remove whitespace at top after replacing imports
When running fixImports, I was seeing empty lines at the top of files.
This issue was introduced when we switched from eslint to babylon for
finding imports, and was easiest to address as the last step of
replacing imports.
Clean out benchmark leftovers
I forgot to remove these in 3ae01ee when I deleted the benchmark.js
file.
Move `importjsd` commands over to `importjs`
We don't need two separate commands for importjs. By merging them we
make some things more discoverable (logpath e.g.), and easier to
maintain.

I'm keeping the old `importjsd` around to avoid breaking clients. When
enough time has passed, we can remove things.
Start ModuleFinder for regular CLI calls
Now that we only have the ModuleFinder to find imports, we need to make
sure it's started whenever we import something.
Prepare for Atom plugin
To make it easier for the Atom plugin to make use of import-js, I've
extracted a function to initialize the module finder in a separate
module.

Also updates version so that we can try out in Atom.
Pass along workingDirectory to config from initializeModuleFinder
Without doing this, the Atom plugin would use the wrong configuration.
Don't import `console`
Adding this to the list of default globals should prevent accidentally
importing something named `console`.
Fix findCurrentImports parsing for let declarations
findCurrentImports would fail if something like this was encountered in
the body of the file:

  let foo;

Adding an early return fixes the issue.

trotzig added some commits Jan 21, 2017

Prevent duplicate paths stored when file is in project root
Before, we would add ['.-baz', 'baz'] to storage for a file ./baz.js.
Ignore watching nested node_modules folders
In projects with nested packages and node_modules folders, we shouldn't
listen to anything in node_modules.
Fix deduping modules
For package dependencies, we don't have a `filePath`. The importPath
should be enough to dedupe things.
Find workingDirectory dynamically from current file
In mono-repos (such as e.g. happo), we have multiple packages with
individual node_modules folders. I want to be able to start my editor
(vim) from the root project dir, and still have importing work for
different package files.

For instance, if I open a file `packages/happo-core/src/server.js` and
import `foo`, I want to only look for files locally in the
packages/happo-core folder.
Add test for findProjectRoot
I pushed this earlier without making sure that it was properly tested.
Always return absolute paths for `goto`
Now that we may end up handling multiple working directories within the
same importjs process, it's better to always return absolute paths when
running `goto`.

As a side-effect, this (in combination with an earlier commit) fixes #178.

trotzig added some commits Jan 21, 2017

Export a better main module
In preparation for updating the Atom plugin, I'm making the main file
export a few things that will be needed.
Fix resolving named imports
If two or more modules matches a variable name, you are asked to
resolve it manually. In case you selected an import that was meant to be
a named import (import { named } from 'module'), we would not honor it
being named, and the import ended up being

  import named from 'module';

I fixed this by asking the module finder once more for the import. This
is now fast since we have the storage cache/index.
Improve findProjectRoot for node_modules
We want to be able to enter a package dependency (using `goto`), then
navigate within that project directly. Because some package dependencies
don't have nested dependencies, I had to take the node_modules folder
check out of the condition that checks if a directory is a valid project
root.
Find exports defined after `module.exports` line
A not too uncommon pattern for packages is to have the `module.exports`
line above the fully defined object. Something like:

  const a = {};
  module.exports = a;
  a.foo = 'foo';

We can make a first sweep to find all defined objects and then use that
for the actual module.exports.
Find exports inside anonymous root functions
It's not uncommon for packages to protect against contaminating global
browser scope by wrapping itself in a self-executing anonymous. By
reaching into these functions when finding exports, we are able to find
a few more exports.
Find exports for underscore.js
Because of how their exported module is constructed, finding named
exports turned out to be a little bit of a challenge. But through a
combination of remembering the defined objects, plus using a regexp to
find the actual `module.exports`, I was able to get it working.

Because this ended up being a little brittle, I added a regression test
for a bug that I was close to introducing.
@lencioni

I only gave this a high-level review. Noted a few things that jumped out at me. I think probably the best next steps are to try it out in a lot of places and fix anything that might be amiss.

It might also be worth enabling flow on more of the files to see if that shakes out any bugs (as well as updating flow to the latest).

fileName,
];
if (/e?s$/.test(dirName)) {

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

A comment might not be a bad idea around here to explain why this is in here.

@lencioni

lencioni Jan 22, 2017

Collaborator

A comment might not be a bad idea around here to explain why this is in here.

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

Good idea. Done in 9a6dcc9

@trotzig

trotzig Jan 22, 2017

Collaborator

Good idea. Done in 9a6dcc9

}
handleFilesAdded(unexpandedFiles) {
return new Promise((resolve) => {

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

With all of these promises, it might be worth spending some time going through and making sure that errors aren't being swallowed.

@lencioni

lencioni Jan 22, 2017

Collaborator

With all of these promises, it might be worth spending some time going through and making sure that errors aren't being swallowed.

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

I've been running this code quite intensely for a month+ now. I hope to have found all the places where we're not handling rejections.

@trotzig

trotzig Jan 22, 2017

Collaborator

I've been running this code quite intensely for a month+ now. I hope to have found all the places where we're not handling rejections.

Show outdated Hide outdated lib/Watcher.js
}
initializePolling(): Promise<void> {
setInterval(() => {

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

Should this have some sort of a shutdown mechanism that disables this interval?

@lencioni

lencioni Jan 22, 2017

Collaborator

Should this have some sort of a shutdown mechanism that disables this interval?

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

It should run as long as the process is active.

To be honest, I'm considering cutting out the polling completely. It's hard to test, and won't provide a good user-experience. What do you think? Hard dependency on watchman? It runs on most os:es.

@trotzig

trotzig Jan 22, 2017

Collaborator

It should run as long as the process is active.

To be honest, I'm considering cutting out the polling completely. It's hard to test, and won't provide a good user-experience. What do you think? Hard dependency on watchman? It runs on most os:es.

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

That makes sense to me. It might be worth considering making this pluggable, so you can use watchman or swap it out for some other thing if you want (e.g. polling, fsevents, or some other file watcher).

@lencioni

lencioni Jan 22, 2017

Collaborator

That makes sense to me. It might be worth considering making this pluggable, so you can use watchman or swap it out for some other thing if you want (e.g. polling, fsevents, or some other file watcher).

excludes: [],
ignorePackagePrefixes: ['react-'],
});
return moduleFinder.initializeStorage(':memory:');

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

I'm not the biggest fan of having a bunch of things happen in beforeEach, because it makes each individual test more difficult to reason about and it can make it more difficult to add new tests to the file. I think this test is maybe small enough and the setup is maybe large enough so that it is okay, but it might be something worth considering inlining into each test for maximum clarity.

@lencioni

lencioni Jan 22, 2017

Collaborator

I'm not the biggest fan of having a bunch of things happen in beforeEach, because it makes each individual test more difficult to reason about and it can make it more difficult to add new tests to the file. I think this test is maybe small enough and the setup is maybe large enough so that it is okay, but it might be something worth considering inlining into each test for maximum clarity.

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

I agree with you in general, but I also think it's fine in this one case. The Importer test is an example of where this has gone way out of proportion (sigh).

@trotzig

trotzig Jan 22, 2017

Collaborator

I agree with you in general, but I also think it's fine in this one case. The Importer test is an example of where this has gone way out of proportion (sigh).

});
}
const DEFAULT_EXPORT_PATTERN = /\smodule\.exports\s*=\s*(\w+)/;

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

If you start this pattern with \s, will it match if module.exports is on the first line without any whitespace before it? You might need to tweak this pattern.

@lencioni

lencioni Jan 22, 2017

Collaborator

If you start this pattern with \s, will it match if module.exports is on the first line without any whitespace before it? You might need to tweak this pattern.

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

Being first on the line is usually (99%) handled by the ast-traversal. This is to cover exports happening deep inside something. The only time ast-traversal wouldn't find it is if the file is using weird indentation, and the module.exports isn't at the root of the tree.

@trotzig

trotzig Jan 22, 2017

Collaborator

Being first on the line is usually (99%) handled by the ast-traversal. This is to cover exports happening deep inside something. The only time ast-traversal wouldn't find it is if the file is using weird indentation, and the module.exports isn't at the root of the tree.

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

I see. What if it is wrapped in parens like this?

(module.exports = Foo)
@lencioni

lencioni Jan 22, 2017

Collaborator

I see. What if it is wrapped in parens like this?

(module.exports = Foo)

This comment has been minimized.

@trotzig

trotzig Jan 23, 2017

Collaborator

We could fix that, but I don't know if it's worth it. There are numerous other cases where the regex won't work (line-breaks, not exporting an identifier, curly braces, etc). It should be seen as a nice-to-have, but not strictly useful.

@trotzig

trotzig Jan 23, 2017

Collaborator

We could fix that, but I don't know if it's worth it. There are numerous other cases where the regex won't work (line-breaks, not exporting an identifier, curly braces, etc). It should be seen as a nice-to-have, but not strictly useful.

Show outdated Hide outdated lib/findJsModulesFor.js
Show outdated Hide outdated lib/findProjectRoot.js
Show outdated Hide outdated lib/importjs.js
}
}
if (parent.type === 'GenericTypeAnnotation') {

This comment has been minimized.

@lencioni

lencioni Jan 22, 2017

Collaborator

Some AST example comments here would be nice too

@lencioni

lencioni Jan 22, 2017

Collaborator

Some AST example comments here would be nice too

This comment has been minimized.

@trotzig

trotzig Jan 22, 2017

Collaborator

Yeah... I'll add to my todo to make a sweep here.

@trotzig

trotzig Jan 22, 2017

Collaborator

Yeah... I'll add to my todo to make a sweep here.

trotzig added some commits Jan 22, 2017

Add comment explaining plural handling for `defaultExportNames`
@lencioni pointed out that this was a little confusing, and I agree.
Adding a comment to explain the use-case a little should help others
reading this code.
Flesh out warning about falling back to polling
This log (previously info level, now warning) should help people
understand that they should install watchman to make the tool better.
Pointed out by @lencioni in code review.
Switch to Set for packageDependencies
This list of strings should never be allowed to contain duplicates. By
using a Set we make a few operations easier, such as avoiding to make a
full scan in one place.
Move a regex to a constant
This will speed up things a little as the regex can be precompiled. I
don't think this matters much tbh, but it's good practice.
Use stdoutWrite for `importjs logpath`
We already have this convenience method defined, so we should use it.
@trotzig

This comment has been minimized.

Show comment
Hide comment
@trotzig

trotzig Jan 23, 2017

Collaborator

The code climate check fails because I'm more than 50 commits ahead of master. https://docs.codeclimate.com/docs/analysis-error-codes#G12

Going to ignore that for now and hope that things return to normal once on master.

Collaborator

trotzig commented Jan 23, 2017

The code climate check fails because I'm more than 50 commits ahead of master. https://docs.codeclimate.com/docs/analysis-error-codes#G12

Going to ignore that for now and hope that things return to normal once on master.

Fix finding named exports in required files
In case the exported thing from a module is a require statement, we
grab exports from the required module and merge with the main module. I
had screwed this up in an earlier commit that refactored a few methods.
@lencioni

This comment has been minimized.

Show comment
Hide comment
@lencioni

lencioni Jan 25, 2017

Collaborator

I'm using beta 7 locally and noticed that it doesn't find expect from chai or shallow from enzyme

I haven't dug into why, but I figured I'd point it out for you.

We also have some imports that are structured like import Foo from 'package-name/Foo';. For now I'll just add these as aliases as I run into them.

Collaborator

lencioni commented Jan 25, 2017

I'm using beta 7 locally and noticed that it doesn't find expect from chai or shallow from enzyme

I haven't dug into why, but I figured I'd point it out for you.

We also have some imports that are structured like import Foo from 'package-name/Foo';. For now I'll just add these as aliases as I run into them.

@trotzig

This comment has been minimized.

Show comment
Hide comment
@trotzig

trotzig Jan 25, 2017

Collaborator

That's good feedback! I'll look into why the exports aren't found.

I made a decision not to reach into package dependencies folders. I think it's considered bad practice to expose internal structure from a package (React is moving away from it). I think the best thing we can do is to file issues/PRs to make these projects use flat bundles.

Collaborator

trotzig commented Jan 25, 2017

That's good feedback! I'll look into why the exports aren't found.

I made a decision not to reach into package dependencies folders. I think it's considered bad practice to expose internal structure from a package (React is moving away from it). I think the best thing we can do is to file issues/PRs to make these projects use flat bundles.

Fix finding exports using `exports.use(...)`
@lencioni pointed out that the exports for chai
(https://github.com/chaijs/chai) weren't all found. It turns out that it
uses

  exports.use(someIdentifier);

which we hadn't included when parsing for exports.
@trotzig

This comment has been minimized.

Show comment
Hide comment
@trotzig

trotzig Jan 25, 2017

Collaborator

I fixed the chai issue. The other one I wasn't able to reproduce. npm install --save enzyme made it possible to import shallow. Maybe it's an old version? Can you try rm $(importjs cachepath) and then try again? One annoying part of that is that I made the watcher initialize lazily, so it won't start until the first time you try importing something. At which time the cache is empty, so you won't get results the first time.

Collaborator

trotzig commented Jan 25, 2017

I fixed the chai issue. The other one I wasn't able to reproduce. npm install --save enzyme made it possible to import shallow. Maybe it's an old version? Can you try rm $(importjs cachepath) and then try again? One annoying part of that is that I made the watcher initialize lazily, so it won't start until the first time you try importing something. At which time the cache is empty, so you won't get results the first time.

trotzig added some commits Jan 25, 2017

Replace loglevel logging with winston
I was running into a lot of issues with the previous logging solution,
which mostly meant rerouting console logs to a file. That didn't work
well when multiple processes were run in parallel.

Winston handles this a lot better, and supports our formatting needs.
Fix finding export where exported identifier is a function
I was getting errors when finding exports in the Brigade codebase. It
turns out to be caused by `module.exports` exporting a function
identifier.
Use ms timestamp on log messages
The previous string was just outputting the date, which is sort of
useless. We could look into a better formatted string but for now the ms
version is actually helpful since I can better debug timing issues.
Show message if cache isn't ready
Now that we initialize storage for a working directory lazily, we've
surfaced (an already existing but not before very visible) race
condition: the exports storage isn't yet populated when you first
import. The scenario is:

- You install import-js for the first time, or update an old version.
- You go to a file and run "fixImports" (or some other command)
- A background thread will start scanning the working directory for
imports. This might take a while (5-10 seconds on the Brigade codebase).
- The result comes back empty the first time.
- You wait for a bit and try again.
- Result is good.

The law of leaky abstractions [1] strikes again. In our effort to hide
complexity (finding exports in a large file system) we've reached a
point where it's tricky to hide the abstraction.

I discussed this with @lencioni, and we came up with a few ideas:
A) Have clients tell import-js about new working directories so that we
can prime the cache.
B) Let the command (fixImports, import, goto) wait until the cache is
ready.
C) Display a message that results are inaccurate.

C is the cheapest one, and that's what I've implemented in this commit.
A is expensive since it means updating all the clients. B is hard to
implement, and may lead to a pretty bad user experience for first-time
use. For instance, the vim plugin will timeout itself after 3s I
believe (built-into the channel infrastructure in vim). I'll explore A
going forward, if this quick-fix turns out to be too naive.

[1] https://www.joelonsoftware.com/2002/11/11/the-law-of-leaky-abstractions/
Include error message in rejected daemon commands
This will improve debugging a little. If the command fails, we should at
least display the error message somehow.
Refactor findExports a little
I'm about to add support for exports on a reassigned `exports` object.
Before I do that, I wanted to make the methods a little more structured
and take option objects instead of just a list of arguments. This should
make the new addition a little simpler.
Find exports in files reassigning `exports`
When working on logging improvements, I added winston as a dependency.
It has a somewhat non-standard way of setting up its imports. In short:

  var winston = exports;
  exports.something = ...;

etc.

By memoizing all the aliases used for `exports`, we can catch exports of
this kind as well.

Also, since it's highly likely that people want to use the default
import from modules like this, we're also assuming a default export in
this situation.
Find exports from json files
Up until now, we haven't been treating json files differently from
js/jsx files. Which meant that babylon failed to parse the data.

By checking if the filename is .json, we can fix this by taking a
shortcut and use JSON.parse directly instead of babylon. It could be
argued that we shouldn't rely on the filename like this, but I think
it's relatively safe. If we find an example that breaks this assumption,
we can address the problem.

@trotzig trotzig merged commit 090a820 into master Jan 31, 2017

2 of 3 checks passed

codeclimate Code Climate encountered an error attempting to analyze this pull request.
Details
codeclimate/coverage 95.03% (-1.3%)
Details
continuous-integration/travis-ci/push The Travis CI build passed
Details

@trotzig trotzig deleted the find-named-exports branch Jan 31, 2017

Show outdated Hide outdated lib/Watcher.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment