Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

babel-plugin-mockable-imports

Package Overview
Dependencies
Maintainers
1
Versions
15
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

babel-plugin-mockable-imports - npm Package Compare versions

Comparing version 1.0.0 to 1.1.0

13

CHANGELOG.md

@@ -8,4 +8,17 @@ # Changelog

## [1.1.0] - 2019-04-08
- Support mocking of CommonJS / Node-style imports (`var aModule = require("a-module")`)
- Support excluding imports from certain modules from being transformed for
mock-ability, via the `excludeImportsFromModules` option
- Support excluding modules from certain directories from being processed by
this plugin via the `excludeDirs` option. By default this is configured to
avoid transforming tests
- Fix CommonJS default exports (`module.exports = <expression>`) overwriting the
`$imports` export
- Use more robust logic to avoid rewriting references to imports in the
initialization of the `$imports` variable in generated code
## [1.0.0] - 2019-04-07
- Initial release

235

index.js
'use strict';
const pathModule = require('path');
const packageName = require('./package.json').name;
const helperImportPath = `${packageName}/lib/helpers`;
function isImportReference(path) {
const name = path.node.name;
const binding = path.scope.getBinding(name, /* noGlobal */ true);
if (!binding) {
const EXCLUDE_LIST = [
// Proxyquirify and proxyquire-universal are two popular mocking libraries
// which include Browserify plugins that look for references to their imports
// in the code. You'd never want to mock these, and applying the transform
// here breaks the plugin.
'proxyquire',
];
const EXCLUDED_DIRS = ['test', '__tests__'];
module.exports = ({types: t}) => {
/**
* Return the required module from a CallExpression, if it is a `require(...)`
* call.
*/
function commomJSRequireSource(node) {
const args = node.arguments;
if (
node.callee.name === 'require' &&
args.length === 1 &&
t.isStringLiteral(args[0])
) {
return args[0].value;
} else {
return null;
}
}
/**
* Return true if imports from the module `source` should not be made
* mockable.
*/
function excludeImportsFromModule(source, state) {
const excludeList = state.opts.excludeImportsFromModules || EXCLUDE_LIST;
return excludeList.includes(source);
}
/**
* Return true if the current module should not be processed at all.
*/
function excludeModule(state) {
const filename = state.file.opts.filename;
if (!filename) {
// No filename was supplied when Babel was run, assume this file should
// be processed.
return false;
}
const excludeList = state.opts.excludeDirs || EXCLUDED_DIRS;
const dirParts = pathModule.dirname(filename).split(pathModule.sep);
return dirParts.some(part => excludeList.includes(part));
}
function isCommonJSExportAssignment(path) {
const assignExpr = path.node;
if (t.isMemberExpression(assignExpr.left)) {
const target = assignExpr.left;
if (t.isIdentifier(target.object) && t.isIdentifier(target.property)) {
return (
target.object.name === 'module' && target.property.name === 'exports'
);
}
}
return false;
}
const importParent = binding.path.findParent(p => p.isImportDeclaration());
return importParent != null;
}
module.exports = ({types: t}) => {
function lastExprInSequence(node) {
if (t.isSequenceExpression(node)) {
return node.expressions[node.expressions.length - 1];
} else {
return node;
}
}
return {

@@ -33,3 +98,7 @@ visitor: {

// create an innert path and use that?
state.aborted = false;
state.aborted = excludeModule(state);
// Set to `true` if a `module.exports = <expr>` expression was seen
// in the module.
state.hasCommonJSExportAssignment = false;
},

@@ -52,3 +121,3 @@

],
t.stringLiteral(helperImportPath)
t.stringLiteral(helperImportPath),
);

@@ -69,25 +138,115 @@

);
const $importsDecl = t.exportNamedDeclaration(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('$imports'),
t.newExpression(t.identifier('ImportMap'), [
importMetaObjLiteral,
]),
),
]),
[
t.exportSpecifier(
t.identifier('$imports'),
t.identifier('$imports'),
),
],
);
const $importsDecl = t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier('$imports'),
t.newExpression(t.identifier('ImportMap'), [
importMetaObjLiteral,
]),
),
]);
const exportImportsDecl = t.exportNamedDeclaration(null, [
t.exportSpecifier(
t.identifier('$imports'),
t.identifier('$imports'),
),
]);
// Insert `$imports` declaration below last import.
const insertedNodes = state.lastImport.insertAfter(helperImport);
insertedNodes[0].insertAfter($importsDecl);
// Insert `export { $imports }` at the end of the file. The reason for
// inserting here is that this gets converted to `exports.$imports =
// $imports` if the file is later transpiled to CommonJS, and this
// must come after any `module.exports = <value>` assignments.
const body = path.get('body');
body[body.length - 1].insertAfter(exportImportsDecl);
// If the module contains a `module.exports = <foo>` expression then
// add `module.exports.$imports = <foo>` at the end of the file.
if (state.hasCommonJSExportAssignment) {
const moduleExportsExpr = t.memberExpression(
t.memberExpression(
t.identifier('module'),
t.identifier('exports'),
),
t.identifier('$imports'),
);
const cjsExport = t.expressionStatement(
t.assignmentExpression(
'=',
moduleExportsExpr,
t.identifier('$imports'),
),
);
body[body.length - 1].insertAfter(cjsExport);
}
},
},
AssignmentExpression(path, state) {
if (!isCommonJSExportAssignment(path)) {
return;
}
state.hasCommonJSExportAssignment = true;
},
CallExpression(path, state) {
const node = path.node;
const source = commomJSRequireSource(node);
if (!source) {
return;
}
if (excludeImportsFromModule(source, state)) {
return;
}
// Ignore requires that are not part of the initializer of a
// `var init = require("module")` statement.
const varDecl = path.findParent(p => p.isVariableDeclarator());
if (!varDecl) {
return;
}
// If the `require` is wrapped in some way, we just ignore it, since
// we cannot determine which symbols are being required without knowing
// what the wrapping expression does.
//
// An exception is made where the `require` statement is wrapped in
// a sequence (`var foo = (..., require('blah'))`) as some code coverage
// transforms do. We know in this case that the result will be the
// result of the require.
if (lastExprInSequence(varDecl.node.init) !== node) {
return;
}
// Ignore non-top level require expressions.
if (varDecl.parentPath.parent.type !== 'Program') {
return;
}
state.lastImport = varDecl.findParent(p => p.isVariableDeclaration());
// `var aModule = require("a-module")`
const id = varDecl.node.id;
if (id.type === 'Identifier') {
state.importMeta.set(id, {
symbol: '<CJS>',
source,
value: id,
});
} else if (id.type === 'ObjectPattern') {
// `var { aSymbol: localName } = require("a-module")`
for (let property of id.properties) {
state.importMeta.set(property.value, {
symbol: property.key.name,
source,
value: property.value,
});
}
}
},
ImportDeclaration(path, state) {

@@ -125,5 +284,10 @@ if (state.aborted) {

const source = path.node.source.value;
if (excludeImportsFromModule(source, state)) {
return;
}
state.importMeta.set(spec.local, {
symbol: imported,
source: path.node.source.value,
source,
value: spec.local,

@@ -135,12 +299,18 @@ });

ReferencedIdentifier(child, state) {
if (state.aborted || !isImportReference(child)) {
if (state.aborted) {
return;
}
const name = child.node.name;
const binding = child.scope.getBinding(name, /* noGlobal */ true);
if (!binding || !state.importMeta.has(binding.identifier)) {
return;
}
// Ignore the reference in the generated `$imports` variable declaration.
const newExprParent = child.findParent(p => p.isNewExpression());
const varDeclParent = child.findParent(p => p.isVariableDeclarator());
if (
newExprParent &&
t.isIdentifier(newExprParent.node.callee) &&
newExprParent.node.callee.name === 'ImportMap'
varDeclParent &&
varDeclParent.node.id.type === 'Identifier' &&
varDeclParent.node.id.name === '$imports'
) {

@@ -181,1 +351,2 @@ return;

};
('use strict');

34

lib/helpers.js

@@ -59,8 +59,9 @@ "use strict";

var sourceImports = imports[source];
var esImports = sourceImports;
if (typeof sourceImports === 'function') {
sourceImports = {
"default": sourceImports
if (typeof esImports === 'function') {
esImports = {
"default": esImports
};
} // Handle namespace imports (`import * as foo from "foo"`).
} // Handle namespace ES imports (`import * as foo from "foo"`).

@@ -75,14 +76,25 @@

namespaceAliases.forEach(function (alias) {
_this[alias] = esImports;
}); // Handle CJS imports (`var foo = require("bar")`).
var cjsAliases = Object.keys(_this.$meta).filter(function (alias) {
var _this$$meta$alias2 = _this.$meta[alias],
source_ = _this$$meta$alias2[0],
symbol_ = _this$$meta$alias2[1];
return source_ === source && symbol_ === '<CJS>';
});
cjsAliases.forEach(function (alias) {
_this[alias] = sourceImports;
}); // Handle named imports (`import { foo } from "..."`).
}); // Handle named ES imports (`import { foo } from "..."`) or
// destructured CJS imports (`var { foo } = require("...")`).
Object.keys(sourceImports).forEach(function (symbol) {
Object.keys(esImports).forEach(function (symbol) {
var aliases = Object.keys(_this.$meta).filter(function (alias) {
var _this$$meta$alias2 = _this.$meta[alias],
source_ = _this$$meta$alias2[0],
symbol_ = _this$$meta$alias2[1];
var _this$$meta$alias3 = _this.$meta[alias],
source_ = _this$$meta$alias3[0],
symbol_ = _this$$meta$alias3[1];
return source_ === source && symbol_ === symbol;
});
if (aliases.length === 0 && namespaceAliases.length === 0) {
if (aliases.length === 0 && namespaceAliases.length === 0 && cjsAliases.length === 0) {
throw new Error("Module does not import \"" + symbol + "\" from \"" + source + "\"");

@@ -92,3 +104,3 @@ }

aliases.forEach(function (alias) {
_this[alias] = sourceImports[symbol];
_this[alias] = esImports[symbol];
});

@@ -95,0 +107,0 @@ });

{
"name": "babel-plugin-mockable-imports",
"version": "1.0.0",
"version": "1.1.0",
"description": "Babel plugin for mocking ES imports",

@@ -5,0 +5,0 @@ "main": "index.js",

@@ -7,3 +7,3 @@ # babel-plugin-mockable-imports

- Provide a simple interface for mocking ES module imports
- Provide a simple interface for mocking ES and CommonJS (`require`) module imports
- Work when running tests in Node with any test runner or in the browser when

@@ -142,5 +142,39 @@ using any bundler (Browserify, Webpack etc.) or no bunder at all

You can use this to prevent the plugin from mocking imports in test modules
for example.
As a convenience, the plugin by default skips any files in directories named
"test", "__tests__" or subdirectories of directories with those names. This
can be configured using the `excludeDirs` option.
### Options
The plugin supports the following options:
`excludeDirs`
An array of directory names (eg. "tests") whose modules are excluded from
this transformation by default.
`excludeImportsFromModules`
An array of module names which should be ignored when processing imports.
Any imports from these modules will not be mockable.
Default: `["proxyquire"]`.
### CommonJS support
The plugin has basic support for CommonJS. It will recognize the
following patterns as imports:
```js
var foo = require('./foo');
var { foo } = require('./foo');
var { foo: bar } = require('./foo');
```
Where `var` may also be `const` or `let`. If the `require` is wrapped or
contained within an expression it will not be processed.
When processing a CommonJS module the plugin still emits ES6 `import` and
`export` declarations, so transforming of ES6 `import`/`export` statements
to CommonJS must be enabled in Babel.
## How it works

@@ -147,0 +181,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc