Search code examples
typescripteslintmonorepo

eslint rule counting total exported identifiers


I have a requirement in my js/ts monorepo to make sure that each lib keeps the amount of exported identifiers low.

Are there existing rules/packages for eslint that count total exported symbols?

export * from './lib/foo' // 4 identifiers exported
export {x,y} from './lib/bar' // 2 identifiers exported
// maybe i have a lint rule that says "max-exports 5" or something, so this would error

I know that in some cases, such as dynamic exports, re-exports from 3p packages, or custom resolvers... that implementing such a rule could be prohibitively challenging.

Nonetheless, an imperfect (but sensible) implementation would be very interesting. I've done cursory google searches and npm searches and come up empty!


Solution

  • Since this is very niche, I am afraid you might be stuck with having to roll your own. Luckily, eslint provides ways to create custom rules.

    In your case, since the limit of exported members would be greater than 1, you can specifically look for instances where the export is an object. This would look something like this:

    const MAX_NUM_EXPORTS = 2;
    
    module.exports = {
      meta: {
        messages: {
          maxExports: `Modules can only contain upto ${MAX_NUM_EXPORTS} exports`
        }
      },
      create(context) {
        return {
          ExportDefaultDeclaration(node) {
            if (
                node.declaration.type === "ObjectExpression" 
                && node.declaration.properties.length > MAX_NUM_EXPORTS  
            ) {
              context.report({ node, messageId: 'maxExports'})
            }
          },
          ExportNamedDeclaration(node) {
            if (node.declaration.type === "VariableDeclaration") {
              node.declaration.declarations.forEach(dec => {
                if (
                    dec.init.type === "ObjectExpression"
                    && dec.init.properties.length > MAX_NUM_EXPORTS
                ) {
                  context.report({ node, messageId: 'maxExports'})
                }
              })
            }
          }
        }
      }
    }
    

    The create method returns callback functions, I used an AST Explorer to figure out which nodes in the tree to tell eslint to look for and run my rule on. Specifically they are the ExportNamedDeclaration node to handle standard variable exports and the ExportNamedDeclaration node to handle default exports.

    Once we have grabbed these nodes we then check to see if they are objects and if those object have more properties than the allowed limits. If not, we throw our error message.

    You can read more about eslint custom rules in the docs mentioned above.

    In order to test that these rules work, you can use the built-in eslint RuleTester class, combined with your favorite testing suit. For demonstration purposes I used the Jest unit testing suit.

    Using the the RuleTester class in a file called rule.test.js would look something like this:

    const { RuleTester } = require('eslint');
    const limitMaxExports = require('./rule');
    
    const ruleTester = new RuleTester(
      {
         parserOptions: { 
           ecmaVersion: 6,
           sourceType: 'module'
         }
      }
    );
    
    const valid = JSON.stringify({
        a: 'this',
        b: 'is valid'
    });
    
    const invalid = JSON.stringify({
        a: 'this',
        b: 'has',
        c: 'too many'
    });
    
    ruleTester.run('limit-max-exports', limitMaxExports, {
      valid: [
        {
          code: `export default ${valid}`
        },
        {
          code: `export const blah = ${valid}`
        }
      ],
      invalid: [
        {
          code: `export default ${invalid}`,
          errors: [{messageId: 'maxExports'}]
        },
        {
          code: `export const invalid = ${invalid}`,
          errors: [{messageId: 'maxExports'}]
        }
      ]
    });
    

    Now when we run the tests jest will print out that we have successfully passed all tests:

    Image of Jest output

    I should note that this is only an example and will only work on es6 modules (as noted in the RuleTester instantiation parameter object).