Skip to content
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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: named: commonjs exports #1226

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
154 changes: 145 additions & 9 deletions src/ExportMap.js
Expand Up @@ -266,14 +266,14 @@ function captureTomDoc(comments) {
}
}

ExportMap.get = function (source, context) {
ExportMap.get = function (source, context, options) {
const path = resolve(source, context)
if (path == null) return null

return ExportMap.for(childContext(path, context))
return ExportMap.for(childContext(path, context), options)
}

ExportMap.for = function (context) {
ExportMap.for = function (context, options = {}) {
const { path } = context

const cacheKey = hashObject(context).digest('hex')
Expand All @@ -300,14 +300,14 @@ ExportMap.for = function (context) {
const content = fs.readFileSync(path, { encoding: 'utf8' })

// check for and cache ignore
if (isIgnored(path, context) || !unambiguous.test(content)) {
if (isIgnored(path, context) || (!options.useCommonjsExports && !unambiguous.test(content))) {
log('ignored path due to unambiguous regex or ignore settings:', path)
exportCache.set(cacheKey, null)
return null
}

log('cache miss', cacheKey, 'for path', path)
exportMap = ExportMap.parse(path, content, context)
exportMap = ExportMap.parse(path, content, context, options)

// ambiguous modules return null
if (exportMap == null) return null
Expand All @@ -319,7 +319,9 @@ ExportMap.for = function (context) {
}


ExportMap.parse = function (path, content, context) {
ExportMap.parse = function (path, content, context, options = {}) {
log('using commonjs exports:', options.useCommonjsExports)

var m = new ExportMap(path)

try {
Expand All @@ -330,7 +332,7 @@ ExportMap.parse = function (path, content, context) {
return m // can't continue
}

if (!unambiguous.isModule(ast)) return null
if (!options.useCommonjsExports && !unambiguous.isModule(ast)) return null

const docstyle = (context.settings && context.settings['import/docstyle']) || ['jsdoc']
const docStyleParsers = {}
Expand Down Expand Up @@ -362,7 +364,7 @@ ExportMap.parse = function (path, content, context) {
function resolveImport(value) {
const rp = remotePath(value)
if (rp == null) return null
return ExportMap.for(childContext(rp, context))
return ExportMap.for(childContext(rp, context), options)
}

function getNamespace(identifier) {
Expand Down Expand Up @@ -390,7 +392,7 @@ ExportMap.parse = function (path, content, context) {
const existing = m.imports.get(p)
if (existing != null) return existing.getter

const getter = () => ExportMap.for(childContext(p, context))
const getter = () => ExportMap.for(childContext(p, context), options)
m.imports.set(p, {
getter,
source: { // capturing actual node reference holds full AST in memory!
Expand All @@ -401,8 +403,118 @@ ExportMap.parse = function (path, content, context) {
return getter
}

// for saving all commonjs exports
let moduleExports = {}

// for if module exports has been declared directly (exports/module.exports = ...)
let moduleExportsMain = null

function parseModuleExportsObjectExpression(node) {
moduleExportsMain = true
moduleExports = {}
node.properties.forEach(
function(property) {
const keyType = property.key.type

if (keyType === 'Identifier') {
const keyName = property.key.name
moduleExports[keyName] = property.value
}
else if (keyType === 'Literal') {
const keyName = property.key.value
moduleExports[keyName] = property.value
}
}
)
}

function handleModuleExports() {
let isEsModule = false
const esModule = moduleExports.__esModule
if (esModule && esModule.type === 'Literal' && esModule.value) {
// for interopRequireDefault calls
}

Object.getOwnPropertyNames(moduleExports).forEach(function (propertyName) {
m.namespace.set(propertyName)
})

if (!isEsModule && moduleExportsMain && !options.noInterop) {
// recognizes default for import statements
m.namespace.set('default')
}
}

ast.body.forEach(function (n) {
if (options.useCommonjsExports) {
if (n.type === 'ExpressionStatement') {
if (n.expression.type === 'AssignmentExpression') {
const left = n.expression.left
const right = n.expression.right

// exports/module.exports = ...
if (isCommonjsExportsObject(left)) {
moduleExportsMain = true

// exports/module.exports = {...}
if (right.type === 'ObjectExpression') {
parseModuleExportsObjectExpression(right)
}
}
else if (left.type === 'MemberExpression'
&& isCommonjsExportsObject(left.object)) {
// (exports/module.exports).<name> = ...
if (left.property.type === 'Identifier') {
const keyName = left.property.name
moduleExports[keyName] = right
}
// (exports/module.exports).["<name>"] = ...
else if (left.property.type === 'Literal') {
const keyName = left.property.value
moduleExports[keyName] = right
}
}
else return
}
// Object.defineProperty((exports/module.exports), <name>, {value: <value>})
else if (n.expression.type === 'CallExpression') {
const call = n.expression

const callee = call.callee
if (callee.type !== 'MemberExpression') return
if (callee.object.type !== 'Identifier' || callee.object.name !== 'Object') return
if (callee.property.type !== 'Identifier' || callee.property.name !== 'defineProperty') return

if (call.arguments.length !== 3) return
if (!isCommonjsExportsObject(call.arguments[0])) return
if (call.arguments[1].type !== 'Literal') return
if (call.arguments[2].type !== 'ObjectExpression') return

call.arguments[2].properties.forEach(function (defineProperty) {
if (defineProperty.type !== 'Property') return

if (defineProperty.key.type === 'Literal'
&& defineProperty.key.value === 'value') {
// {'value': <value>}
Object.defineProperty(
moduleExports,
call.arguments[1].value,
defineProperty.value
)
}
else if (defineProperty.key.type === 'Identifier'
&& defineProperty.key.name === 'value') {
// {value: <value>}
Object.defineProperty(
moduleExports,
call.arguments[1].value,
defineProperty.value
)
}
})
}
}
}

if (n.type === 'ExportDefaultDeclaration') {
const exportMeta = captureDoc(docStyleParsers, n)
Expand Down Expand Up @@ -483,6 +595,8 @@ ExportMap.parse = function (path, content, context) {
}
})

if (options.useCommonjsExports) handleModuleExports()

return m
}

Expand Down Expand Up @@ -527,3 +641,25 @@ function childContext(path, context) {
path,
}
}

/**
* Check if a given node is exports, module.exports, or module['exports']
* @param {node} node
* @return {boolean}
*/
function isCommonjsExportsObject(node) {
// exports
if (node.type === 'Identifier' && node.name === 'exports') return true

if (node.type !== 'MemberExpression') return false

if (node.object.type === 'Identifier' && node.object.name === 'module') {
// module.exports
if (node.property.type === 'Identifier' && node.property.name === 'exports') return true

// module['exports']
if (node.property.type === 'Literal' && node.property.value === 'exports') return true
}

return false
}
29 changes: 28 additions & 1 deletion src/rules/named.js
Expand Up @@ -7,9 +7,31 @@ module.exports = {
docs: {
url: docsUrl('named'),
},
schema : [{
type: 'object',
properties: {
commonjs: {
oneOf: [
{ type: 'boolean' },
{
type: 'object',
properties: {
require: { type: 'boolean' },
exports: { type: 'boolean' },
},
},
],
},
},
additionalProperties: false,
}],
},

create: function (context) {
const options = context.options[0] || {}
const { commonjs = {} } = options
const useCommonjsExports = typeof commonjs === 'boolean' ? commonjs : commonjs.exports

function checkSpecifiers(key, type, node) {
// ignore local exports and type imports
if (node.source == null || node.importKind === 'type') return
Expand All @@ -19,7 +41,12 @@ module.exports = {
return // no named imports/exports
}

const imports = Exports.get(node.source.value, context)
const exportsOptions = {
useCommonjsExports,
noInterop: false, // this should only be true when using require() calls
}

const imports = Exports.get(node.source.value, context, exportsOptions)
if (imports == null) return

if (imports.errors.length) {
Expand Down
5 changes: 5 additions & 0 deletions tests/files/module-exports-direct.js
@@ -0,0 +1,5 @@
exports = {
a: 0,
1: 1,
'c': 2,
}
1 change: 1 addition & 0 deletions tests/files/module-exports-number.js
@@ -0,0 +1 @@
module.exports = 5
5 changes: 5 additions & 0 deletions tests/files/named-default-export-es5.js
@@ -0,0 +1,5 @@
Object.defineProperty(exports, '__esModule', { value: true })

const foo = 'foo'

exports.default = foo
15 changes: 15 additions & 0 deletions tests/files/named-exports-es5.js
@@ -0,0 +1,15 @@
Object.defineProperty(exports, '__esModule', {value: true})

exports.destructuredProp = {}

module.exports.arrayKeyProp = null

exports.deepSparseElement = []

exports.a = 1
exports.b = 2

const c = 3
module.exports.d = c

exports.ExportedClass = class {}
46 changes: 46 additions & 0 deletions tests/files/re-export-default-es5.js
@@ -0,0 +1,46 @@
/* compiled with babel */
'use strict'

Object.defineProperty(exports, '__esModule', {
value: true,
})

var _defaultExport = require('./default-export')

Object.defineProperty(exports, 'bar', {
enumerable: true,
get: function get() {
return _interopRequireDefault(_defaultExport).default
},
})

var _namedDefaultExport = require('./named-default-export')

Object.defineProperty(exports, 'foo', {
enumerable: true,
get: function get() {
return _interopRequireDefault(_namedDefaultExport).default
},
})

var _common = require('./common')

Object.defineProperty(exports, 'common', {
enumerable: true,
get: function get() {
return _interopRequireDefault(_common).default
},
})

var _t = require('./t')

Object.defineProperty(exports, 't', {
enumerable: true,
get: function get() {
return _interopRequireDefault(_t).default
},
})

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj } }

exports.baz = 'baz? really?'
8 changes: 8 additions & 0 deletions tests/files/re-export-names-es5.js
@@ -0,0 +1,8 @@
const namedExports = require('./named-exports-es5')

module.exports = {
__esModule: true,
foo: namedExports.a,
bar: namedExports.b,
baz: 'will it blend?',
}