├── .editorconfig ├── .gitattributes ├── .gitignore ├── .travis.yml ├── index.js ├── license ├── package.json ├── readme.md ├── test ├── _utils.js ├── babel-5-test.js ├── babel-6-test.js ├── fixtures │ ├── exports.js │ └── fixture.js ├── mocha.opts └── register-babel.js └── wrap-listener.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '6' 4 | - '5' 5 | - '4' 6 | - '0.12' 7 | - '0.10' 8 | after_success: npm run coveralls 9 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var wrapListener = require('./wrap-listener'); 3 | 4 | function listener(path, file, opts) { 5 | (path.isLiteral() ? addString : addExpression)(path.node, file, opts); 6 | } 7 | 8 | function addString(node, file, opts) { 9 | var val = attachNodes(opts) ? node : node.value; 10 | requireMetadata(file).strings.push(val); 11 | } 12 | 13 | function addExpression(node, file, opts) { 14 | var val; 15 | 16 | if (attachNodes(opts)) { 17 | val = node; 18 | } else { 19 | val = {start: node.start, end: node.end}; 20 | val.loc = { 21 | start: copyLoc(node.loc.start), 22 | end: copyLoc(node.loc.end) 23 | }; 24 | } 25 | 26 | if (attachExpressionSource(opts)) { 27 | val.code = file.code.slice(val.start, val.end); 28 | } 29 | 30 | requireMetadata(file).expressions.push(val); 31 | 32 | return val; 33 | } 34 | 35 | function copyLoc(loc) { 36 | return loc && {line: loc.line, column: loc.column}; 37 | } 38 | 39 | function requireMetadata(file) { 40 | var metadata = file.metadata; 41 | 42 | if (!metadata.requires) { 43 | metadata.requires = { 44 | strings: [], 45 | expressions: [] 46 | }; 47 | } 48 | 49 | return metadata.requires; 50 | } 51 | 52 | // OPTION EXTRACTION: 53 | 54 | function attachExpressionSource(opts) { 55 | return Boolean(opts && opts.source); 56 | } 57 | 58 | function attachNodes(opts) { 59 | return Boolean(opts && opts.nodes); 60 | } 61 | 62 | module.exports = wrapListener(listener, 'detective'); 63 | 64 | module.exports.metadata = function extractMetadataFromResult(result) { 65 | return result.metadata.requires; 66 | }; 67 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) James Talmage (github.com/jamestalmage) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-detective", 3 | "version": "2.0.0", 4 | "description": "Babel 5/6 plugin that scans the AST for require calls and import statements", 5 | "license": "MIT", 6 | "repository": "avajs/babel-plugin-detective", 7 | "author": { 8 | "name": "James Talmage", 9 | "email": "james@talmage.io", 10 | "url": "github.com/jamestalmage" 11 | }, 12 | "engines": { 13 | "node": ">=0.10.0" 14 | }, 15 | "scripts": { 16 | "test": "xo && nyc mocha", 17 | "coveralls": "nyc report --reporter=text-lcov | coveralls" 18 | }, 19 | "files": [ 20 | "index.js", 21 | "wrap-listener.js" 22 | ], 23 | "keywords": [ 24 | "babel-plugin", 25 | "detective", 26 | "require", 27 | "import", 28 | "dependencies", 29 | "scan", 30 | "traverse" 31 | ], 32 | "devDependencies": { 33 | "babel": "^5.8.34", 34 | "babel-core": "^6.1.21", 35 | "babel-preset-es2015": "^6.1.18", 36 | "babel-preset-stage-3": "^6.1.18", 37 | "coveralls": "^2.11.4", 38 | "mocha": "^2.3.3", 39 | "nyc": "^6.4.4", 40 | "xo": "^0.16.0" 41 | }, 42 | "xo": { 43 | "ignores": [ 44 | "test/_utils.js" 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-detective [![Build Status](https://travis-ci.org/avajs/babel-plugin-detective.svg?branch=master)](https://travis-ci.org/avajs/babel-plugin-detective) [![Coverage Status](https://coveralls.io/repos/github/avajs/babel-plugin-detective/badge.svg?branch=master)](https://coveralls.io/avajs/babel-plugin-detective?branch=master) 2 | 3 | > Babel 5/6 plugin that scans the AST for require calls and import statements 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install --save babel-plugin-detective babel-core 10 | ``` 11 | 12 | 13 | ## Usage 14 | 15 | ```js 16 | import babel from 'babel-core'; 17 | 18 | const detective = require('babel-plugin-detective'); 19 | const myModule = require('my' + 'module'); 20 | 21 | // See below for available options 22 | const options = {}; 23 | 24 | // Babel 5 25 | // `position` can be 'before' or 'after' 26 | let result = babel.transformFileSync(path, { 27 | plugins: [{transformer: detective, position: position}], 28 | extra: { 29 | detective: options 30 | } 31 | }); 32 | 33 | // Babel 6 34 | result = babel.transformFileAsync('/path/to/file', { 35 | plugins:[['detective', options]] 36 | }); 37 | 38 | // metadata will be stored on `result.metadata.requires` 39 | // a convenience method is provided to extract it. 40 | const metadata = detective.metadata(result); 41 | 42 | console.log(metadata); 43 | // { 44 | // strings: [ 45 | // 'babel-core', 46 | // 'babel-plugin-detective' 47 | // ], 48 | // expressions: [ 49 | // {start: 110, end: 125, loc: {...}} // loc of 'my' + 'module' expression 50 | // ] 51 | // } 52 | ``` 53 | 54 | ## API 55 | 56 | ### detective.metadata(previousParseResult) 57 | 58 | During traversal, the plugin stores each discovered require/import on the Babel metadata object. 59 | It can be extracted manually from `parseResult.metadata.requires`, or you can pass the parse result 60 | to this convenience method. 61 | 62 | ## Returned Metadata 63 | 64 | After a babel traversal with this plugin, metadata will be attached at `parseResult.metadata.requires` 65 | 66 | ### requires.strings 67 | 68 | Type: `Array` 69 | 70 | Array of every module imported with an ES2015 import statement, and every module `required` using a string literal (it does not include dynamic requires). 71 | 72 | ### requires.expressions 73 | 74 | Type: `Array` 75 | 76 | Array of location data for expressions that are used as the first argument to `require` (i.e. dynamic requires). 77 | The source of the expression can be attached using the `attachExpressionSource` option. 78 | If you wish to disallow dynamic requires, you should throw if this has length greater than 0. 79 | 80 | ## Options 81 | 82 | ### options.generated 83 | 84 | Type: `boolean` 85 | Default: `false` 86 | 87 | If set to true, it will include `require` calls generated by previous plugins in the 88 | tool chain. This will lead to some duplicate entries if ES2015 import statements are 89 | present in the file. This plugin already scans for ES2015 import statements, so you 90 | only need to use this if there is some other type of generated require statement you 91 | want to know about. 92 | 93 | *Works on Babel 6 only* 94 | 95 | `generated:true` can be combined with `import:false` to get only the `require` 96 | statements of the post transform code. 97 | 98 | ### options.import 99 | 100 | Type: `boolean`
101 | Default: `true` 102 | 103 | Include ES2015 imports in the metadata. All ES2015 imports will be of type `string`. 104 | 105 | ### options.export 106 | 107 | Type: `boolean`
108 | Default: `true` 109 | 110 | Include ES2015 re-exports in the metadata. All ES2015 re-exports will be of type `string`. 111 | 112 | ```js 113 | export * from './foo'; 114 | export {bar as baz} from './quz'; 115 | export {hello} from './goodbye'; 116 | ``` 117 | 118 | ### options.require 119 | 120 | Type: `boolean`
121 | Default: `true` 122 | 123 | Include CommonJS style `require(...)` statements in the metadata. CommonJS require statements will be pushed on to `requires.strings` if the argument is a string literal. For dynamic expressions (i.e. `require(foo + bar)`), an object will be pushed on to `requires.expressions`. It will have `start` and `end` properties that can be used to extract the code directly from the original source, And a `loc` object that includes the line/column numbers (useful for creating error statements). 124 | 125 | ### options.word 126 | 127 | Type: `string`
128 | Default: `'require'` 129 | 130 | The name of the require function. You most likely do not need to change this. 131 | 132 | ### options.source 133 | 134 | Type: `boolean`
135 | Default: `false` 136 | 137 | Attach the actual expression code to each member of `requires.expressions`. 138 | 139 | ```js 140 | expressions: [ 141 | {start: 110, end: 125, loc: {...} code: "'my' + 'module'"} 142 | ] 143 | ``` 144 | 145 | ### options.nodes 146 | 147 | Type: `boolean`
148 | Default: `false` 149 | 150 | Return the actual nodes instead of extracting strings. 151 | 152 | ```js 153 | strings : [{type: 'StringLiteral', value: 'foo', ...}], 154 | expressions: [ 155 | {type: 'BinaryExpression', ...} 156 | ] 157 | ``` 158 | 159 | Everything in `strings` will be a `Literal` (*Babel 5*), or `StringLiteral` (*Babel 6*). The path required will be on `node.value`. 160 | 161 | The `expressions` array can contain any valid `Expression` node. 162 | 163 | ## Manipulating Require Statements 164 | 165 | *Warning: Exploratory Support Only*: The documentation here is intentionally sparse. While every attempt will be made to avoid breaking changes, it is a new feature so changes are a real possibility. You should look at the source of `index.js` and the test suite for a better idea on how to use this. 166 | 167 | `babel-detective/wrap-listener` allows you to create your own plugin that can manipulate exports. 168 | 169 | The following creates a plugin that upper-cases all require and import statements: 170 | 171 | ```js 172 | const wrapListener = require('babel-detective/wrap-listener'); 173 | 174 | module.exports = wrapListener(listener, 'uppercase'); 175 | 176 | function listener(path, file, opts) { 177 | if (path.isLiteral()) { 178 | path.node.value = path.node.value.toUpperCase(); 179 | } 180 | } 181 | ``` 182 | 183 | ### wrapListener(listener, name, options) 184 | 185 | #### listener 186 | 187 | Type: `callback(nodePath, file, opts)` 188 | *Required* 189 | 190 | A listener that performs the actual manipulation. It is called with: 191 | 192 | - `nodePath`: The actual node in question can be accessed via `nodePath.node`. `nodePath` has other properties (`parent`, `isLiteral()`, etc.). A full description of that API is out of scope for this document. 193 | 194 | - `file`: The Babel file metadata object. This is what the main module uses to store metadata for required modules that it finds. 195 | 196 | - `opts`: The options that were passed to the plugin. This is done via the array syntax in Babel 6, or `options.extra[name]` in Babel 5. 197 | 198 | 199 | #### name 200 | 201 | *Required*
202 | Type: `string` 203 | 204 | This is the key used to locate `opts` in Babel 5 `options.extra[name]`. 205 | 206 | #### options 207 | 208 | *Optional* 209 | 210 | Accepts the `import`, `require`, and `generated` options as described above. 211 | 212 | 213 | ## Related 214 | 215 | - [`node-detective`](https://github.com/substack/node-detective) Inspiration for this module. Used by `browserify` to analyze module dependencies. 216 | 217 | 218 | ## License 219 | 220 | MIT © [James Talmage](https://github.com/jamestalmage) 221 | -------------------------------------------------------------------------------- /test/_utils.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import detective from '../'; 4 | 5 | export function getFixturePath(fixtureFile) { 6 | return path.resolve(__dirname, 'fixtures', fixtureFile); 7 | } 8 | 9 | export function getFixtureContents(fixtureFile) { 10 | return fs.readFileSync(getFixturePath(fixtureFile), 'utf8'); 11 | } 12 | 13 | export function replaceExpressions(metadata, file) { 14 | const contents = getFixtureContents(file); 15 | metadata.expressions = metadata.expressions.map(loc => contents.slice(loc.start, loc.end)); 16 | } 17 | 18 | export function metadata(parseResult, replaceExp) { 19 | const metadata = detective.metadata(parseResult); 20 | if (replaceExp) { 21 | replaceExpressions(metadata, parseResult.options.filename); 22 | } 23 | return metadata; 24 | } 25 | -------------------------------------------------------------------------------- /test/babel-5-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | import assert from 'assert'; 3 | import * as babel from 'babel'; 4 | import detective from '../'; 5 | import {getFixturePath, metadata} from './_utils'; 6 | 7 | function parseFixture(fixtureFile, position, opts) { 8 | var path = getFixturePath(fixtureFile); 9 | var result = babel.transformFileSync(path, { 10 | plugins: [{transformer: detective, position: position}], 11 | extra: { 12 | detective: opts || {} 13 | } 14 | }); 15 | 16 | result.options = {filename: path}; 17 | 18 | return result; 19 | } 20 | 21 | describe('babel-5', function () { 22 | it('produces a list of expressions', () => { 23 | const parseResult = parseFixture('fixture.js'); 24 | assert.deepEqual(metadata(parseResult), { 25 | strings: ['b', 'foo'], 26 | expressions: [{ 27 | start: 60, 28 | end: 73, 29 | loc: { 30 | start: { 31 | line: 7, 32 | column: 8 33 | }, 34 | end: { 35 | line: 7, 36 | column: 21 37 | } 38 | } 39 | }] 40 | }); 41 | assert.deepEqual(metadata(parseResult, true), { 42 | strings: ['b', 'foo'], 43 | expressions: [`'foo' + 'bar'`] 44 | }); 45 | }); 46 | 47 | it('before builtin plugins', () => { 48 | const parseResult = parseFixture('fixture.js', 'before'); 49 | assert.deepEqual(metadata(parseResult), { 50 | strings: ['b', 'foo'], 51 | expressions: [{ 52 | start: 60, 53 | end: 73, 54 | loc: { 55 | start: { 56 | line: 7, 57 | column: 8 58 | }, 59 | end: { 60 | line: 7, 61 | column: 21 62 | } 63 | } 64 | }] 65 | }); 66 | assert.deepEqual(metadata(parseResult, true), { 67 | strings: ['b', 'foo'], 68 | expressions: [`'foo' + 'bar'`] 69 | }); 70 | }); 71 | 72 | it('after builtin plugins', () => { 73 | const parseResult = parseFixture('fixture.js', 'after'); 74 | assert.deepEqual(metadata(parseResult), { 75 | strings: ['b', 'foo'], 76 | expressions: [{ 77 | start: 60, 78 | end: 73, 79 | loc: { 80 | start: { 81 | line: 7, 82 | column: 8 83 | }, 84 | end: { 85 | line: 7, 86 | column: 21 87 | } 88 | } 89 | }] 90 | }); 91 | assert.deepEqual(metadata(parseResult, true), { 92 | strings: ['b', 'foo'], 93 | expressions: [`'foo' + 'bar'`] 94 | }); 95 | }); 96 | 97 | it('alternate word', () => { 98 | const parseResult = parseFixture('fixture.js', 'before', {word: '__dereq__'}); 99 | assert.deepEqual(metadata(parseResult), { 100 | strings: ['b', 'baz'], 101 | expressions: [] 102 | }); 103 | }); 104 | 105 | it('imports can be excluded', () => { 106 | const parseResult = parseFixture('fixture.js', 'before', {import: false}); 107 | assert.deepEqual(metadata(parseResult, true), { 108 | strings: ['foo'], 109 | expressions: [`'foo' + 'bar'`] 110 | }); 111 | }); 112 | 113 | it('require statements can be excluded', () => { 114 | const parseResult = parseFixture('fixture.js', 'before', {require: false}); 115 | 116 | assert.deepEqual(metadata(parseResult), { 117 | strings: ['b'], 118 | expressions: [] 119 | }); 120 | }); 121 | 122 | it('attachExpressionSource attaches code to location object', () => { 123 | const parseResult = parseFixture('fixture.js', 'after', {source: true}); 124 | 125 | assert.deepEqual(metadata(parseResult), { 126 | strings: ['b', 'foo'], 127 | expressions: [{ 128 | start: 60, 129 | end: 73, 130 | code: `'foo' + 'bar'`, 131 | loc: { 132 | start: { 133 | line: 7, 134 | column: 8 135 | }, 136 | end: { 137 | line: 7, 138 | column: 21 139 | } 140 | } 141 | }] 142 | }); 143 | }); 144 | 145 | it('options.nodes', () => { 146 | const data = metadata(parseFixture('fixture.js', 'after', {nodes: true})); 147 | const strings = data.strings; 148 | const expressions = data.expressions; 149 | 150 | assert.strictEqual(strings.length, 2); 151 | assert.strictEqual(strings[0].type, 'Literal'); 152 | assert.strictEqual(strings[0].value, 'b'); 153 | assert.strictEqual(strings[1].type, 'Literal'); 154 | assert.strictEqual(strings[1].value, 'foo'); 155 | 156 | assert.strictEqual(expressions.length, 1); 157 | assert.strictEqual(expressions[0].type, 'BinaryExpression'); 158 | assert.strictEqual(expressions[0].code, undefined); 159 | assert.strictEqual(expressions[0].operator, '+'); 160 | assert.strictEqual(expressions[0].left.type, 'Literal'); 161 | assert.strictEqual(expressions[0].left.value, 'foo'); 162 | assert.strictEqual(expressions[0].right.type, 'Literal'); 163 | assert.strictEqual(expressions[0].right.value, 'bar'); 164 | }); 165 | 166 | it('options.nodes', () => { 167 | const data = metadata(parseFixture('fixture.js', 'after', {nodes: true, source: true})); 168 | const expressions = data.expressions; 169 | 170 | assert.strictEqual(expressions.length, 1); 171 | assert.strictEqual(expressions[0].type, 'BinaryExpression'); 172 | assert.strictEqual(expressions[0].code, `'foo' + 'bar'`); 173 | }); 174 | 175 | it('handles exports', () => { 176 | const {strings} = metadata(parseFixture('exports.js', 'after')); 177 | 178 | assert.deepEqual(strings, ['./foo', './quz', './goodbye']); 179 | }); 180 | }); 181 | -------------------------------------------------------------------------------- /test/babel-6-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, mocha */ 2 | import assert from 'assert'; 3 | import * as babel from 'babel-core'; 4 | import detective from '../'; 5 | import {getFixturePath, metadata} from './_utils'; 6 | 7 | function parseFixture(fixtureFile, opts) { 8 | opts = opts || {}; 9 | const presets = opts.presets || []; 10 | const plugins = opts.plugins || [[detective, opts]]; 11 | const babelOpts = { 12 | presets: presets, 13 | plugins: plugins 14 | }; 15 | 16 | return babel.transformFileSync(getFixturePath(fixtureFile), babelOpts); 17 | } 18 | 19 | describe('babel-6', function () { 20 | it('produces a list of expressions', () => { 21 | assert.deepEqual(metadata(parseFixture('fixture.js')), { 22 | strings: ['b', 'foo'], 23 | expressions: [{ 24 | start: 60, 25 | end: 73, 26 | loc: { 27 | start: { 28 | line: 7, 29 | column: 8 30 | }, 31 | end: { 32 | line: 7, 33 | column: 21 34 | } 35 | } 36 | }] 37 | }); 38 | }); 39 | 40 | it('works with es2015 preset', () => { 41 | var parseResult = parseFixture('fixture.js', {presets: ['es2015']}); 42 | assert.deepEqual(metadata(parseResult), { 43 | strings: ['b', 'foo'], 44 | expressions: [{ 45 | start: 60, 46 | end: 73, 47 | loc: { 48 | start: { 49 | line: 7, 50 | column: 8 51 | }, 52 | end: { 53 | line: 7, 54 | column: 21 55 | } 56 | } 57 | }] 58 | }); 59 | assert.deepEqual(metadata(parseResult, true), { 60 | strings: ['b', 'foo'], 61 | expressions: [`'foo' + 'bar'`] 62 | }); 63 | }); 64 | 65 | it('including generated will cause duplicate results (one from the import, one from the generated require)', () => { 66 | var parseResult = parseFixture('fixture.js', { 67 | presets: ['es2015'], 68 | plugins: [[detective, {generated: true}]] 69 | }); 70 | assert.deepEqual(metadata(parseResult), { 71 | strings: ['b', 'foo', 'b'], 72 | expressions: [{ 73 | start: 60, 74 | end: 73, 75 | loc: { 76 | start: { 77 | line: 7, 78 | column: 8 79 | }, 80 | end: { 81 | line: 7, 82 | column: 21 83 | } 84 | } 85 | }] 86 | }); 87 | assert.deepEqual(metadata(parseResult, true), { 88 | strings: ['b', 'foo', 'b'], 89 | expressions: [`'foo' + 'bar'`] 90 | }); 91 | }); 92 | 93 | it('alternate word', () => { 94 | var parseResult = parseFixture('fixture.js', { 95 | presets: ['es2015'], 96 | plugins: [[detective, {word: '__dereq__'}]] 97 | }); 98 | assert.deepEqual(metadata(parseResult), { 99 | strings: ['b', 'baz'], 100 | expressions: [] 101 | }); 102 | }); 103 | 104 | it('import statements can be excluded', () => { 105 | var parseResult = parseFixture('fixture.js', { 106 | presets: ['es2015'], 107 | plugins: [[detective, {import: false}]] 108 | }); 109 | assert.deepEqual(metadata(parseResult, true), { 110 | strings: ['foo'], 111 | expressions: [`'foo' + 'bar'`] 112 | }); 113 | }); 114 | 115 | it('require statements can be excluded', () => { 116 | var parseResult = parseFixture('fixture.js', { 117 | plugins: [[detective, {require: false}]] 118 | }); 119 | 120 | assert.deepEqual(metadata(parseResult), { 121 | strings: ['b'], 122 | expressions: [] 123 | }); 124 | }); 125 | 126 | it('attachExpressionSource attaches code to location object', () => { 127 | var parseResult = parseFixture('fixture.js', { 128 | plugins: [[detective, {source: true}]] 129 | }); 130 | 131 | assert.deepEqual(metadata(parseResult), { 132 | strings: ['b', 'foo'], 133 | expressions: [{ 134 | start: 60, 135 | end: 73, 136 | code: `'foo' + 'bar'`, 137 | loc: { 138 | start: { 139 | line: 7, 140 | column: 8 141 | }, 142 | end: { 143 | line: 7, 144 | column: 21 145 | } 146 | } 147 | }] 148 | }); 149 | }); 150 | 151 | it('options.nodes', () => { 152 | const data = metadata(parseFixture('fixture.js', { 153 | plugins: [[detective, {nodes: true}]] 154 | })); 155 | const strings = data.strings; 156 | const expressions = data.expressions; 157 | 158 | assert.strictEqual(strings.length, 2); 159 | assert.strictEqual(strings[0].type, 'StringLiteral'); 160 | assert.strictEqual(strings[0].value, 'b'); 161 | assert.strictEqual(strings[1].type, 'StringLiteral'); 162 | assert.strictEqual(strings[1].value, 'foo'); 163 | 164 | assert.strictEqual(expressions.length, 1); 165 | assert.strictEqual(expressions[0].type, 'BinaryExpression'); 166 | assert.strictEqual(expressions[0].code, undefined); 167 | assert.strictEqual(expressions[0].operator, '+'); 168 | assert.strictEqual(expressions[0].left.type, 'StringLiteral'); 169 | assert.strictEqual(expressions[0].left.value, 'foo'); 170 | assert.strictEqual(expressions[0].right.type, 'StringLiteral'); 171 | assert.strictEqual(expressions[0].right.value, 'bar'); 172 | }); 173 | 174 | it('attach source to nodes', () => { 175 | const data = metadata(parseFixture('fixture.js', { 176 | plugins: [[detective, {nodes: true, source: true}]] 177 | })); 178 | const expressions = data.expressions; 179 | 180 | assert.strictEqual(expressions.length, 1); 181 | assert.strictEqual(expressions[0].type, 'BinaryExpression'); 182 | assert.strictEqual(expressions[0].code, `'foo' + 'bar'`); 183 | }); 184 | 185 | it('handles exports', () => { 186 | const {strings} = metadata(parseFixture('exports.js', { 187 | presets: ['es2015'] 188 | })); 189 | 190 | assert.deepEqual(strings, ['./foo', './quz', './goodbye']); 191 | }); 192 | }); 193 | -------------------------------------------------------------------------------- /test/fixtures/exports.js: -------------------------------------------------------------------------------- 1 | export * from './foo'; 2 | export {bar as baz} from './quz'; 3 | export {hello} from './goodbye'; 4 | 5 | export default function () {} 6 | 7 | export function foo() {} 8 | -------------------------------------------------------------------------------- /test/fixtures/fixture.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import b from 'b'; 4 | 5 | require('foo'); 6 | 7 | require('foo' + 'bar'); 8 | 9 | __dereq__('baz'); 10 | 11 | require(); 12 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --compilers js:./test/register-babel.js 2 | -------------------------------------------------------------------------------- /test/register-babel.js: -------------------------------------------------------------------------------- 1 | require('babel-core/register')({ 2 | presets: [ 3 | 'stage-3', 4 | 'es2015' 5 | ], 6 | only: ['_utils.js', '*-test.js'] 7 | }); 8 | -------------------------------------------------------------------------------- /wrap-listener.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function isBabel5(babel) { 4 | if (!babel.Plugin) { 5 | return false; 6 | } 7 | 8 | if (!babel.version) { 9 | return true; 10 | } 11 | 12 | return /^5\./.test(babel.version); 13 | } 14 | 15 | module.exports = function (listener, name, options) { 16 | return function detective(babel) { 17 | if (isBabel5(babel)) { 18 | // Babel 5 19 | return new babel.Plugin('detective', {visitor: { 20 | ImportDeclaration: function (a, b, c, file) { 21 | return visitImportDeclaration(this, file, file.opts.extra[name]); 22 | }, 23 | CallExpression: function (a, b, c, file) { 24 | return visitCallExpression(this, file, file.opts.extra[name]); 25 | } 26 | }}); 27 | } 28 | // Babel 6 29 | return { 30 | visitor: { 31 | ImportDeclaration: function (path, state) { 32 | return visitImportDeclaration(path, state.file, state.opts); 33 | }, 34 | CallExpression: function (path, state) { 35 | return visitCallExpression(path, state.file, state.opts); 36 | }, 37 | ExportNamedDeclaration: function (path, state) { 38 | return visitExportDeclaration(path, state.file, state.opts); 39 | }, 40 | ExportAllDeclaration: function (path, state) { 41 | return visitExportDeclaration(path, state.file, state.opts); 42 | } 43 | } 44 | }; 45 | }; 46 | 47 | function visitExportDeclaration(path, file, opts) { 48 | if (includeExports(opts) && path.get('source').node) { 49 | listener(path.get('source'), file, opts); 50 | } 51 | } 52 | 53 | function visitImportDeclaration(path, file, opts) { 54 | if (includeImports(opts)) { 55 | listener(path.get('source'), file, opts); 56 | } 57 | } 58 | 59 | function visitCallExpression(path, file, opts) { 60 | if (!includeRequire(opts)) { 61 | return; 62 | } 63 | 64 | var callee = path.get('callee'); 65 | 66 | if (callee.isIdentifier() && callee.node.name === word(opts)) { 67 | var arg = path.get('arguments.0'); 68 | 69 | if (arg && (!arg.isGenerated() || includeGenerated(opts))) { 70 | listener(arg, file, opts); 71 | } 72 | } 73 | } 74 | 75 | // OPTION EXTRACTION: 76 | 77 | function word(opts) { 78 | opts = options || opts; 79 | return (opts && opts.word) || 'require'; 80 | } 81 | 82 | function includeGenerated(opts) { 83 | opts = options || opts; 84 | return Boolean(opts && opts.generated); 85 | } 86 | 87 | function includeImports(opts) { 88 | opts = options || opts; 89 | return (!opts || opts.import) !== false; 90 | } 91 | 92 | function includeExports(opts) { 93 | opts = options || opts; 94 | return (!opts || opts.export) !== false; 95 | } 96 | 97 | function includeRequire(opts) { 98 | opts = options || opts; 99 | return (!opts || opts.require) !== false; 100 | } 101 | }; 102 | --------------------------------------------------------------------------------