Skip to content

Commit

Permalink
feat: initial CommonJS module support
Browse files Browse the repository at this point in the history
  • Loading branch information
gabriel.rohden committed Dec 18, 2021
1 parent 3b2f7e7 commit 731701a
Show file tree
Hide file tree
Showing 10 changed files with 322 additions and 40 deletions.
59 changes: 40 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ This plugin parses and resolves a barrel file

> What is a barrel file?
Usually, index files with a lot of export statements, more info [here](https://basarat.gitbook.io/typescript/main-1/barrel)
Usually, index files with a lot of export statements, more
info [here](https://basarat.gitbook.io/typescript/main-1/barrel)

## But really, what it does?

Given a module that export its dependencies using a barrel file like this:

```js
// lodash/index.js
// my-lib/index.js
export { map } from './dist/map';
export { chain } from './dist/chain';
export { filter } from './dist/filter';
Expand All @@ -21,18 +22,18 @@ export { groupBy } from './dist/groupBy';
And that you import it like this:

```js
import { map, chain } from 'lodash';
import { map, chain } from 'my-lib';
```

it will transform your code to:

```js
import { map } from 'lodash/dist/map';
import { chain } from 'lodash/dist/chain';
import { map } from 'my-lib/dist/map';
import { chain } from 'my-lib/dist/chain';
```

Since a barrel file exports all files from a lib, babel/bundlers usually will
import and parse all of those files because they can have side effects.
Since a barrel file exports all files from a lib, babel/bundlers usually will import and parse all of those files
because they can have side effects.

This plugin will drop unused imports from barrel files at lib roots, which will also remove import side effects.

Expand All @@ -44,19 +45,38 @@ Because React Native sucks (or I suck because I don't know how to do this in met

## Not supported

#### CommonJS

Common JS files are not supported currently.

#### Full imports

Right now, this plugin doesn't support full imports like:

```js
import _ from 'lodash'
import all from 'my-lib'
```

We could ignore this, in the future and just warn that this kind of import make this plugin useless.

#### Barrel effects and local exports

Since this plugin is meant to make the barrel file 'invisible' to the bundler, it will not resolve local exports.

a barrel file like this:

```ts
export default "foo";

if (!x) {
throw Error("foo")
}
```

We could ignore this, in the future and just warn that this kind of
import make this plugin useless.
Will do nothing

#### CommonJS dynamic stuff

CommonJS is supported, although a lot of corner cases are not supported because I wrote myself the code to find and
track exports. (PR welcome, would love to improve the CJS support)

Usually, a babel generated CommonJS will work fine.

## Installation

Expand All @@ -80,10 +100,10 @@ module.exports = {
'resolve-barrel-files',
{
'my-lib': {
barrelFilePath: path.resolve(
require.resolve('lodash'),
'../lib/module'
)
moduleType: 'commonjs', // or 'esm'
barrelFilePath: path.resolve(require.resolve('my-lib'))
// if you want to debug this plugin
// logLevel: "debug" | "info"
},
},
]
Expand All @@ -97,4 +117,5 @@ Feel free to open a PR, or an issue and I'll try to help you :D

### Mentions

Heavily inspired by [babel-plugin-transform-imports](https://bitbucket.org/amctheatres/babel-transform-imports/src/master/)
Heavily inspired
by [babel-plugin-transform-imports](https://bitbucket.org/amctheatres/babel-transform-imports/src/master/)
36 changes: 30 additions & 6 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,30 @@ const pathLib = require("path");
const types = require("@babel/types");

const { err, partition } = require("./src/misc");
const { collectExports } = require("./src/collect-exports");
const { collectEsmExports } = require("./src/collect-esm-exports");
const { collectCjsExports } = require("./src/collect-cjs-exports");
const { resolveLogLevel, DEBUG, INFO } = require("./src/log");

const cachedResolvers = {};

function getOrCacheExports({
function getCachedExports({
moduleName,
barrelFilePath,
moduleType,
}) {
if (cachedResolvers[moduleName]) {
return cachedResolvers[moduleName];
}

return cachedResolvers[moduleName] = collectExports(
require.resolve(barrelFilePath),
);
if (moduleType === "esm") {
cachedResolvers[moduleName] = collectEsmExports(barrelFilePath);
}

if (moduleType === "commonjs") {
cachedResolvers[moduleName] = collectCjsExports(barrelFilePath);
}

return cachedResolvers[moduleName];
}

module.exports = function() {
Expand All @@ -32,9 +41,15 @@ module.exports = function() {

const transforms = [];
const sourceImport = sourceConfig.mainBarrelPath;
const exports = getOrCacheExports({
const moduleType = sourceConfig.moduleType || "commonjs";
const logLevel = resolveLogLevel(sourceConfig.logLevel);

logLevel >= INFO && console.log(`[${moduleName}] Resolving ${moduleType} imports from ${sourceImport}`);

const exports = getCachedExports({
moduleName,
barrelFilePath: sourceImport,
moduleType,
});

const [fullImports, memberImports] = partition(
Expand All @@ -50,6 +65,15 @@ module.exports = function() {
const importName = memberImport.imported.name;
const localName = memberImport.local.name;
const exportInfo = exports[importName];

if (!exports[importName]) {
logLevel >= DEBUG
&& console.log(
`[${moduleName}] No export info found for ${importName}, are you sure this is a ${moduleType} module?`,
);
continue;
}

const importFrom = pathLib.join(sourceImport, exportInfo.importPath);

let newImportSpecifier = memberImport;
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "babel-plugin-resolve-barrel-files",
"version": "0.0.1",
"version": "0.0.2",
"description": "A babel plugin that solves typescript barrel files",
"main": "index.js",
"repository": "https://github.com/Grohden/babel-plugin-resolve-barrel-files",
Expand Down
136 changes: 136 additions & 0 deletions src/collect-cjs-exports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
const fs = require("fs");

const ts = require("typescript");

const isObjectDefineProperty = (child) => {
return child.kind === ts.SyntaxKind.ExpressionStatement
&& child.expression.kind === ts.SyntaxKind.CallExpression
&& child.expression.expression.kind === ts.SyntaxKind.PropertyAccessExpression
&& child.expression.expression.name.text === "defineProperty";
};

const findReturnStatement = (child) => {
return child.initializer.body.statements.find(statement => ts.SyntaxKind.ReturnStatement === statement.kind);
};

/**
* Given a call expression returns the require call expression either from
* direct require call or from wrapped _interopRequireDefault call
*
* eg:
* require("./buzz"); // returns require("./buzz")
* _interopRequireDefault(require("./buzz")); // returns require("./buzz")
*
* if its not a require call, returns null
*/
const getRequireCallData = (child) => {
if (child.kind === ts.SyntaxKind.CallExpression) {
const callExpression = child.expression;

if (callExpression.kind === ts.SyntaxKind.Identifier) {
if (callExpression.text === "_interopRequireDefault") {
return getRequireCallData(child.arguments[0]);
}

if (callExpression.text === "require") {
return child;
}
}
}

return null;
};

/**
* Given an child node, checks if it
* is a require variable statement like
*
* var _foo = require('foo');
*
* and if it is, returns the require value and the variable name
* else returns null
*/
const getRequireVariableStatementData = (child) => {
if (
!child.kind === ts.SyntaxKind.VariableStatement && child.expression.kind === ts.SyntaxKind.VariableDeclarationList
) {
return null;
}

const [declaration] = child.declarationList?.declarations || [];
if (declaration?.kind !== ts.SyntaxKind.VariableDeclaration) {
return null;
}

const callData = getRequireCallData(declaration.initializer);
if (!callData) {
return null;
}

return {
name: declaration.name.text,
value: callData.arguments[0].text,
};
};

/**
* Parses a CJS barrel (index) file, extracts all it's export
* names and returns an object that maps
* a import name to the path + some meta infos.
*
* Note: this doesn't handle dynamic imports.
*/
const collectCjsExports = (file) => {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file).toString(),
ts.ScriptTarget.ES2015,
true,
);

const definePropsResolvers = {};
const requireResolvers = {};

sourceFile.forEachChild((child) => {
// collects cjs exports
if (isObjectDefineProperty(child)) {
const [exportsTarget, accessorName, definition] = child.expression.arguments;

if (exportsTarget.text !== "exports") {
return;
}

if (accessorName.text === "__esModule") {
// Should we validate that the value is true?
return;
}

const [, value] = definition.properties;

if (value.name.text === "get") {
const returnStatement = findReturnStatement(value).expression;

definePropsResolvers[accessorName.text] = {
variableName: returnStatement.expression.text,
accessor: returnStatement.name.text,
};
}
}

const requireData = getRequireVariableStatementData(child);
if (requireData) {
requireResolvers[requireData.name] = requireData.value;
}
});

return Object.entries(definePropsResolvers).reduce((acc, [importName, data]) => {
acc[importName] = {
importPath: requireResolvers[data.variableName],
importAlias: data.accessor === importName ? null : data.accessor,
};

return acc;
}, {});
};

module.exports = { collectCjsExports };
6 changes: 3 additions & 3 deletions src/collect-exports.js → src/collect-esm-exports.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const fs = require("fs");
const ts = require("typescript");

/**
* Parses a barrel (index) file, extracts all it's export
* Parses a ESM barrel (index) file, extracts all it's export
* names and returns an object that maps
* a import name to the path + some meta infos.
*
Expand All @@ -18,7 +18,7 @@ const ts = require("typescript");
*
* The case above is not supported.
*/
const collectExports = (file) => {
const collectEsmExports = (file) => {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file).toString(),
Expand Down Expand Up @@ -47,4 +47,4 @@ const collectExports = (file) => {
return exports;
};

module.exports = { collectExports };
module.exports = { collectEsmExports };
19 changes: 19 additions & 0 deletions src/log.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const DEBUG = 1;
const INFO = 2;

function resolveLogLevel(level) {
switch (level) {
case "debug":
return DEBUG;
case "info":
return INFO;
default:
return 0;
}
}

module.exports = {
DEBUG,
INFO,
resolveLogLevel,
};
Loading

0 comments on commit 731701a

Please sign in to comment.