├── test ├── imports │ ├── another-file.js │ ├── test-file.js │ ├── client │ │ └── client-test.js │ ├── server │ │ └── server-test.js │ └── package-test │ │ ├── plain-file.js │ │ ├── client │ │ └── index.js │ │ └── server │ │ └── index.js ├── .meteor │ ├── packages │ └── versions ├── custom │ └── .meteor │ │ └── versions └── paths.js ├── .gitignore ├── .travis.yml ├── package.json ├── index.js └── README.md /test/imports/another-file.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/imports/test-file.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.meteor/packages: -------------------------------------------------------------------------------- 1 | test:package 2 | -------------------------------------------------------------------------------- /test/imports/client/client-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/imports/server/server-test.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.meteor/versions: -------------------------------------------------------------------------------- 1 | http 2 | meteor 3 | -------------------------------------------------------------------------------- /test/custom/.meteor/versions: -------------------------------------------------------------------------------- 1 | tracker 2 | -------------------------------------------------------------------------------- /test/imports/package-test/plain-file.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /test/imports/package-test/client/index.js: -------------------------------------------------------------------------------- 1 | export default 'test-client' 2 | -------------------------------------------------------------------------------- /test/imports/package-test/server/index.js: -------------------------------------------------------------------------------- 1 | export default 'test-server' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "node" 5 | - "6" 6 | - "5" 7 | 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-import-resolver-meteor", 3 | "version": "0.3.4", 4 | "description": "Meteor import resolution plugin for eslint-plugin-import.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "test:watch": "mocha --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/clayne11/eslint-import-resolver-meteor" 13 | }, 14 | "keywords": [ 15 | "eslint", 16 | "eslintplugin", 17 | "esnext", 18 | "modules", 19 | "eslint-plugin-import" 20 | ], 21 | "author": "Ben Mosher (me@benmosher.com)", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/clayne11/eslint-import-resolver-meteor/issues" 25 | }, 26 | "homepage": "https://github.com/clayne11/eslint-import-resolver-meteor", 27 | "dependencies": { 28 | "object-assign": "^4.0.1", 29 | "resolve": "^1.1.6" 30 | }, 31 | "peerDependencies": { 32 | "eslint-plugin-import": ">=1.4.0" 33 | }, 34 | "devDependencies": { 35 | "chai": "^3.4.1", 36 | "mocha": "^2.3.4", 37 | "react-dom": "^15.3.0" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var resolve = require('resolve') 2 | var path = require('path') 3 | var assign = require('object-assign') 4 | var fs = require('fs') 5 | 6 | var notNodeModuleRe = /^(\.|\/)/ 7 | var clientRe = /\/client(\/|$)/ 8 | var serverRe = /\/server(\/|$)/ 9 | 10 | exports.interfaceVersion = 2 11 | 12 | exports.resolve = function (source, file, config) { 13 | const meteorDir = config && config.meteorDir; 14 | if (resolve.isCore(source)) return { found: true, path: null } 15 | 16 | if (source.startsWith('meteor/')) { 17 | var meteorRoot = findMeteorRoot(file, meteorDir) 18 | return resolveMeteorPackage(source, meteorRoot) 19 | } 20 | 21 | var meteorSource = source 22 | if (source.startsWith('/')) { 23 | var meteorRoot = findMeteorRoot(file, meteorDir) 24 | meteorSource = path.resolve(meteorRoot, source.substr(1)) 25 | } 26 | 27 | var fileUsingSlash = file.split(path.sep).join('/') 28 | if (!isNodeModuleImport(source) && (isClientInServer(source, fileUsingSlash) || isServerInClient(source, fileUsingSlash))) { 29 | return { found: false } 30 | } 31 | 32 | try { 33 | return { found: true, path: resolve.sync(meteorSource, opts(file, config)) } 34 | } catch (err) { 35 | return { found: false } 36 | } 37 | } 38 | 39 | function opts(file, config) { 40 | return assign({}, 41 | config, 42 | { 43 | // path.resolve will handle paths relative to CWD 44 | basedir: path.dirname(path.resolve(file)), 45 | packageFilter: packageFilter, 46 | }) 47 | } 48 | 49 | function packageFilter(pkg, path, relativePath) { 50 | if (pkg['jsnext:main']) { 51 | pkg['main'] = pkg['jsnext:main'] 52 | } 53 | return pkg 54 | } 55 | 56 | function findMeteorRoot(start, meteorDir) { 57 | start = start || module.parent.filename 58 | meteorDir = meteorDir || ''; 59 | if (typeof start === 'string') { 60 | if (start[start.length-1] !== path.sep) { 61 | start += path.sep 62 | } 63 | start = start.split(path.sep) 64 | } 65 | if(!start.length) { 66 | throw new Error('.meteor not found in path') 67 | } 68 | start.pop() 69 | var dir = start.join(path.sep) 70 | 71 | try { 72 | fs.statSync(path.join(dir, meteorDir, '.meteor')) 73 | return path.join(dir, meteorDir) 74 | } catch (e) {} 75 | return findMeteorRoot(start, meteorDir) 76 | } 77 | 78 | function isNodeModuleImport(source) { 79 | return !notNodeModuleRe.test(source) 80 | } 81 | 82 | function isClientInServer(source, file) { 83 | return serverRe.test(source) && clientRe.test(file) 84 | } 85 | 86 | function isServerInClient(source, file) { 87 | return clientRe.test(source) && serverRe.test(file) 88 | } 89 | 90 | function resolveMeteorPackage(source, meteorRoot) { 91 | try { 92 | var package = source.split('/')[1] 93 | var packageCheckFile = package.indexOf(':') !== -1 ? 94 | getPackageFile(meteorRoot) : 95 | getVersionFile(meteorRoot) 96 | var found = new RegExp('^' + package + '(?:@.*?)?(?:[ \s\t]*#.*)?$', 'm').test(packageCheckFile) 97 | return found ? 98 | { found: found, path: null } : 99 | { found: false } 100 | } catch (e) { 101 | return { found: false } 102 | } 103 | } 104 | 105 | function getVersionFile(meteorRoot) { 106 | var filePath = path.join(meteorRoot, '.meteor', 'versions') 107 | var fileBuffer = fs.readFileSync(filePath) 108 | return fileBuffer.toString() 109 | } 110 | 111 | function getPackageFile(meteorRoot) { 112 | var filePath = path.join(meteorRoot, '.meteor', 'packages') 113 | var fileBuffer = fs.readFileSync(filePath) 114 | return fileBuffer.toString() 115 | } 116 | -------------------------------------------------------------------------------- /test/paths.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect 2 | 3 | var path = require('path') 4 | var meteorResolver = require('../index.js') 5 | 6 | function replaceSlashWithPathSep(filePath) { 7 | return filePath.replace(/\//g, path.sep); 8 | } 9 | 10 | describe('paths', function () { 11 | it('handles base path relative to CWD', function () { 12 | expect(meteorResolver.resolve('./another-file', replaceSlashWithPathSep('./test/imports/test-file.js'))) 13 | .to.have.property('path') 14 | .equal(path.resolve(__dirname, './imports/another-file.js')) 15 | }) 16 | 17 | it('handles root (/) paths relative to CWD', function () { 18 | expect(meteorResolver.resolve('/imports/another-file', replaceSlashWithPathSep('./test/imports/test-file.js'))) 19 | .to.have.property('path') 20 | .equal(path.resolve(__dirname, './imports/another-file.js')) 21 | }) 22 | 23 | it('should not resolve a client file in a server file', function () { 24 | expect(meteorResolver.resolve('/imports/client/client-test', replaceSlashWithPathSep('./test/imports/server/server-test.js'))) 25 | .to.deep.equal({found: false}) 26 | }) 27 | 28 | it('should resolve a client file in a non-client file which is not inside a server folder', function () { 29 | expect(meteorResolver.resolve('/imports/client/client-test', replaceSlashWithPathSep('./test/imports/package-test/plain-file.js'))) 30 | .to.have.property('found', true) 31 | }) 32 | 33 | it('should not resolve a server file in a client file', function () { 34 | expect(meteorResolver.resolve('/imports/server/server-test', replaceSlashWithPathSep('./test/imports/client/client-test.js'))) 35 | .to.deep.equal({found: false}) 36 | }) 37 | 38 | it('should resolve a server file in a non-server file which is not inside a client folder', function () { 39 | expect(meteorResolver.resolve('/imports/server/server-test', replaceSlashWithPathSep('./test/imports/package-test/plain-file.js'))) 40 | .to.have.property('found', true) 41 | }) 42 | 43 | it(`should resolve a file ending in server in a non-server file if it comes from a node module`, function () { 44 | expect(meteorResolver.resolve('react-dom/server', replaceSlashWithPathSep('./test/imports/package-test/plain-file.js'))) 45 | .to.have.property('found', true) 46 | }) 47 | 48 | it('should resolve a custom Meteor package if it is in the packages file', function () { 49 | expect(meteorResolver.resolve('meteor/test:package', replaceSlashWithPathSep('./test/imports/client/client-test.js'))) 50 | .to.deep.equal({ 51 | found: true, 52 | path: null 53 | }) 54 | }) 55 | 56 | it('should not resolve a custom Meteor package if it is not in the packages file', function () { 57 | expect(meteorResolver.resolve('meteor/fake:package', replaceSlashWithPathSep('./test/imports/client/client-test.js'))) 58 | .to.deep.equal({found: false}) 59 | }) 60 | 61 | it('should resolve a built-in Meteor package if it is in the versions file', function () { 62 | expect(meteorResolver.resolve('meteor/meteor', replaceSlashWithPathSep('./test/imports/client/client-test.js'))) 63 | .to.deep.equal({ 64 | found: true, 65 | path: null 66 | }) 67 | }) 68 | 69 | it('should not resolve a built-in Meteor package if it is not in the versions file', function () { 70 | expect(meteorResolver.resolve('meteor/email', replaceSlashWithPathSep('./test/imports/client/client-test.js'))) 71 | .to.deep.equal({found: false}) 72 | }) 73 | 74 | it('should resolve meteor packages in a custom meteor direcotry', function() { 75 | expect(meteorResolver.resolve('meteor/tracker', replaceSlashWithPathSep('./test/imports/client/client-test.js'), { 76 | meteorDir: 'custom' 77 | })).to.deep.equal({ found: true, path: null }) 78 | }) 79 | }) 80 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eslint-import-resolver-meteor 2 | 3 | [![Build Status](https://travis-ci.org/clayne11/eslint-import-resolver-meteor.svg?branch=master)](https://travis-ci.org/clayne11/eslint-import-resolver-meteor) 4 | 5 | Meteor module resolution plugin for [`eslint-plugin-import`](https://www.npmjs.com/package/eslint-plugin-import). 6 | 7 | [On npm](https://www.npmjs.com/package/eslint-import-resolver-meteor) 8 | 9 | ## Installation 10 | 11 | ```javascript 12 | npm install --save-dev eslint eslint-plugin-import eslint-import-resolver-meteor 13 | ``` 14 | 15 | Config is passed directly through to [`resolve`](https://www.npmjs.com/package/resolve#resolve-sync-id-opts) as options: 16 | 17 | In your `.eslintrc.yml`: 18 | ```yaml 19 | settings: 20 | import/resolver: 21 | meteor: 22 | extensions: 23 | # if unset, default is just '.js', but it must be re-added explicitly if set 24 | - .js 25 | - .jsx 26 | - .es6 27 | - .coffee 28 | 29 | paths: 30 | # an array of absolute paths which will also be searched 31 | # think NODE_PATH 32 | - /usr/local/share/global_modules 33 | 34 | # this is technically for identifying `node_modules` alternate names 35 | moduleDirectory: 36 | 37 | - node_modules # defaults to 'node_modules', but... 38 | - bower_components 39 | 40 | - project/src # can add a path segment here that will act like 41 | # a source root, for in-project aliasing (i.e. 42 | # `import MyStore from 'stores/my-store'`) 43 | ``` 44 | 45 | or to use the default options: 46 | 47 | ```yaml 48 | settings: 49 | import/resolver: meteor 50 | ``` 51 | 52 | ## Motivations 53 | 54 | The resolver handles Meteor specific resolutions: 55 | 56 | ### Resolve `/` imports 57 | 58 | The parent directory of the project's `.meteor` folder is used as the root for any `/` paths. 59 | 60 | Example: 61 | 62 | ```javascript 63 | // foo.js 64 | import bar from '/imports/bar' 65 | ``` 66 | 67 | will import from `PROJECT_ROOT/imports/bar`. 68 | 69 | ### Ensure client and server files are imported correctly 70 | Files in a `client` folder should only be able to imported into other files in `client` folders. Likewise, files in a `server` folder should only be able to be imported into other `server` folders. This resolver checks for these cases and will not resolve files that don't follow these rules. 71 | 72 | See the `test/paths.js` file for tests that show these rules. 73 | 74 | 75 | ### Resolve meteor package imports 76 | 77 | The resolver also resolves `import foo from 'meteor/foo:bar`, however this part of the resolver does not work perfectly. 78 | 79 | Meteor packages (ie `import foo from 'meteor/foo:bar'`) do not have a reliable way to access 80 | the main export of a package, and in fact some packages do not even have a main module file but 81 | rather rely on the Meteor build system to generate an importable symbol. This happens in the case of 82 | `api.export('Foo')` rather than using the newer `api.mainModule('index.js')`. 83 | 84 | The strategy for resolving a Meteor import is as follows: 85 | 86 | 1. If the package is a Meteor internal package (ie `import {Meteor} from 'meteor/meteor'`) check that the package exists in `.meteor/versions` so that users don't have to import all internal packages such as Mongo and Meteor directly. 87 | 1. If it is a user created package (ie `import {SimpleSchema} from 'meteor/aldeed:simple-schema'`) check that the package exists in `.meteor/packages`. For user created packages we enforce that if you want to `import from 'meteor/foo:bar'` a file you must `meteor add foo:bar` 88 | 89 | This strategy is imperfect, however it is the best we can do. It leads to the following false positives: 90 | 91 | 1. If you're linting inside of a Meteor package, that package will only have access to the packages that it imports 92 | in it's `package.js` file. You will get false positives for packages that are required by the project but not by the package. 93 | 94 | Even given these limitations, this resolver should still help significantly to lint Meteor projects. 95 | --------------------------------------------------------------------------------