FogelAI / babel-plugin-transform-barrels

A Babel plugin that transforms indirect imports through a barrel file (index.js) into direct imports.
22 stars 2 forks source link

Support for mocks #6

Open pkratz opened 1 month ago

pkratz commented 1 month ago

This is a tremendous plugin! Thank you for your hard work with this. I was wondering if you had plans on adding support for mocks, such that the mocked modules get replaced with the non-barrel import?

FogelAI commented 1 month ago

Hi @pkratz Thank you for your compliment! I really appreciate it! I think I understand what you mean, but I just want to be sure. Could you provide a short code example to clarify it?

pkratz commented 1 month ago

@FogelAI, I setup a public repo that will hopefully illustrate the mock issue. I also encountered a couple of other issues. They are possibly bugs with the plugin or just my misuse of the plugin and re-exports within my own project. The public repo is a bit of contrived example just to illustrate some of the things I'm seeing. I know some of the re-export stuff of @mui libraries might not make sense in this context, but in my larger project these types of re-exports are legitimate.

The repo is located at https://github.com/pkratz/barrel-example

The following are the issues I've run into.

Mocks

Run the following to target the test suite with module mocking.

node "node_modules/jest/bin/jest.js" "./packages/cart-service/src/lib/addToCartWithMock.spec.ts" -c "./packages/cart-service/jest.config.ts" --no-cache

The following are the logs from the run. I am printing out the transformed source code to hopefully better illustrate the problem.

mock.log

The following block from the transformed addToCartWithMock.spec.ts is the relevant block in the test.

"use strict";

// Mock the inventory-service module
_getJestObj().mock('@kratz/inventory-service', () => ({
  checkProductAvailability: jest.fn(),
  reduceProductStock: jest.fn()
}));
var _addToCart = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\cart-service\\src\\lib\\addToCart.ts");
var _cart = require("./cart");
var _checkProductAvailability = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\checkProductAvailability.ts");
var _reduceProductStock = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\reduceProductStock.ts");
function _getJestObj() {
  const {
    jest
  } = require("@jest/globals");
  _getJestObj = () => jest;
  return jest;
}

This is the relevant block in the transformed addToCart.ts, which is used in the test

"use strict";

exports.__esModule = true;
exports.addToCart = void 0;
var _checkProductAvailability = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\checkProductAvailability.ts");
var _reduceProductStock = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\reduceProductStock.ts");
var _cart = require("./cart");

So the checkProductAvailability function that we are calling within addToCart ultimately gets imported from C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\checkProductAvailability.ts, which is great because we don't end up bringing in the whole inventory-service package. However, the addToCartWithMock test is using jest.mock to mock the modules functionality, but that mock is still referring to the module location as @kratz/inventory-service it was not updated to reflect the new import location of C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\inventory-service\\src\\lib\\checkProductAvailability.ts

The tests ultimately error out with the following:

TypeError: checkProductAvailabilityMock.mockReturnValue is not a function

      31 |   test('should add product to cart when product is available and stock is sufficient', () => {
      32 |     // Mock the inventory-service functions
    > 33 |     checkProductAvailabilityMock.mockReturnValue(true);

Named exports with Object Pattern

Some of my packages have named exports that are causing the plugin to update my import to undefined

Run the following to target the test suite with this problem.

node "node_modules/jest/bin/jest.js" "./apps/barrel-example/src/app/ConnectedWelcome.spec.tsx" -c "./apps/barrel-example/jest.config.ts" --no-cache The following are the logs from the run. I am printing out the transformed source code to hopefully better illustrate the problem.

named_object_pattern.log

2024-10-17 17:04:06 - Processed Javascript file: C:\apps\projects\git\sandbox\barrel-example\apps\barrel-example\src\app\ConnectedWelcome.spec.tsx
2024-10-17 17:04:06 - Source import line: import { UserContextProvider } from '@kratz/security';
2024-10-17 17:04:06 - Transformed import line: import UserContextProvider from "undefined";
2024-10-17 17:04:06 - Source import line: import { ConnectedWelcome } from './ConnectedWelcome';
2024-10-17 17:04:06 - Source import line: import { render } from '@testing-library/react';
2024-10-17 17:04:06 - Source import line: import UserContextProvider from "undefined";

FAIL barrel-example apps/barrel-example/src/app/ConnectedWelcome.spec.tsx
  ● Test suite failed to run

    TypeError: C:\apps\projects\git\sandbox\barrel-example\apps\barrel-example\src\app\ConnectedWelcome.spec.tsx: Cannot read properties of undefined (reading 'absEsmFile')

      56 |   return function (sourceText, sourcePath, config) {
      57 |     // run the transformer and return
    > 58 |     const transformedSource = babelTransformer.process(
         |                                                ^
      59 |       sourceText,
      60 |       sourcePath,
      61 |       config

My '@kratz/security' library has the following export defined in UserContext.tsx which ultimately gets re-exported via a barrel index.

export const {
  ContextProvider: UserContextProvider,
  useContext: useUserContext,
} = buildContextBundle<UserContext>({
  defaultValue: {},
  displayName: 'UserContext',
});

That is the export that is triggering the problem. I think there might need to be a an update to barrel#handleExportNamedDeclaration to account for this condition. Possibly when iterating over the declaration via the forEach, check if the declaration is of type "ObjectPattern" and then handle each property explicitly.

declarations.forEach((declaration) => {
        if (
          declaration.id &&
          declaration.id.type === "ObjectPattern" &&
          declaration.id.properties &&
          declaration.id.properties.length > 0
        ) {
          declaration.id.properties.forEach((property) => {
            const specifierObj = SpecifierFactory.createSpecifier("export");
            specifierObj.type = "named";
            specifierObj.esmPath = this.path;

            specifierObj.localName = property.value.name;
            specifierObj.exportedName = property.value.name;
            const { exportedName } = specifierObj;
            specifierObj.esmPath = PathFunctions.normalizeModulePath(specifierObj.esmPath);
            if (!this.defaultPatternExport.isMatchDefaultPattern(specifierObj)) {
              this.exportMapping[exportedName] = specifierObj;
            }
          });
        } else {
          const specifierObj = SpecifierFactory.createSpecifier("export");
          specifierObj.type = "named";
          specifierObj.esmPath = this.path;

          specifierObj.localName = declaration.id.name;
          specifierObj.exportedName = declaration.id.name;
          const { exportedName } = specifierObj;
          specifierObj.esmPath = PathFunctions.normalizeModulePath(specifierObj.esmPath);
          if (!this.defaultPatternExport.isMatchDefaultPattern(specifierObj)) {
            this.exportMapping[exportedName] = specifierObj;
          }
        }
      });

Type exports causing resets

I've also observed something strange when my barrel file is re-exporting type declaration from other libraries. In this example, I have a "core-ui" package that is acting as a wrapper on top of @mui, so it is explicitly re-exporting types.

The barrel file in question is https://github.com/pkratz/barrel-example/blob/main/packages/core-ui/src/index.ts

Run the following to target the test suite with this problem.

node "node_modules/jest/bin/jest.js" "./apps/barrel-example/src/app/Welcome.spec.tsx" -c "./apps/barrel-example/jest.config.ts" --no-cache The following are the logs from the run.

type_export.log

The following is the transformed source of Welcome.tsx


"use strict";

exports.__esModule = true;
exports.Welcome = Welcome;
var _coreUi = require("@kratz/core-ui");
var _jsxDevRuntime = require("react/jsx-dev-runtime");
var _jsxFileName = "C:\\apps\\projects\\git\\sandbox\\barrel-example\\apps\\barrel-example\\src\\app\\Welcome.tsx";
function Welcome(props) {
  return /*#__PURE__*/(0, _jsxDevRuntime.jsxDEV)(_coreUi.Typography, {
    children: ["Welcome, ", props.name]
  }, void 0, true, {
    fileName: _jsxFileName,
    lineNumber: 8,
    columnNumber: 10
  }, this);
}

Typography is being imported by requiring all of coreui from "@kratz/core-ui". I would have expected that this import explicitly target the Typography export from the "@kratz/core-ui" library and be something like:

var _Typography = require("C:\\apps\\projects\\git\\sandbox\\barrel-example\\packages\\core-ui\\src\\lib\\Typography.tsx");

There is something about the type exports that is triggering the barrel to "resetProperties".

fast-json-patch weirdness

In my larger project we make use of the fast-json-patch npm library. I can ultimately explicitly mock this library to work around the problem I noticed, but I wanted to bring it to your attention in case it's a more systemic issue with the plugin that should be addressed.

You can run the following command to trigger this problem.

node "node_modules/jest/bin/jest.js" "./packages/cart-service/src/lib/compareCartChanges.spec.ts" -c "./packages/cart-service/jest.config.ts" --no-cache This is testing a function that is in turn utilizing fast-json-patch. The following are the logs from the run.

fast_json_patch.log

I see the following relevant failure:

2024-10-17 17:21:44 - Processed Javascript file: C:\apps\projects\git\sandbox\barrel-example\packages\cart-service\src\lib\compareCartChanges.ts
2024-10-17 17:21:44 - Source import line: import { compare } from 'fast-json-patch';
2024-10-17 17:21:44 - Transformed import line: import { compare } from "fast-json-patch\\module\\duplex.js";
2024-10-17 17:21:44 - Source import line: import { compare } from "fast-json-patch\\module\\duplex.js";

FAIL cart-service packages/cart-service/src/lib/compareCartChanges.spec.ts
  ● Test suite failed to run

    TypeError: C:\apps\projects\git\sandbox\barrel-example\packages\cart-service\src\lib\compareCartChanges.ts: Cannot read properties of undefined (reading 'absEsmFile')

      56 |   return function (sourceText, sourcePath, config) {
      57 |     // run the transformer and return
    > 58 |     const transformedSource = babelTransformer.process(
         |                                                ^
      59 |       sourceText,
      60 |       sourcePath,
      61 |       config

      at PluginPass.call (../../node_modules/babel-plugin-transform-barrels/src/main.js:24:79)
      at call (../../node_modules/@babel/traverse/src/visitors.ts:303:14)
      at NodePath.call [as _call] (../../node_modules/@babel/traverse/src/path/context.ts:36:20)
      at NodePath.call (../../node_modules/@babel/traverse/src/path/context.ts:21:18)
      at NodePath.visit (../../node_modules/@babel/traverse/src/path/context.ts:97:31)
      at TraversalContext.visitQueue (../../node_modules/@babel/traverse/src/context.ts:148:16)
      at TraversalContext.visitMultiple (../../node_modules/@babel/traverse/src/context.ts:99:17)
      at TraversalContext.visit (../../node_modules/@babel/traverse/src/context.ts:178:19)

Please let me know if you need any clarification on these examples. Thanks!

FogelAI commented 1 month ago

@pkratz Thank you for your detailed explanation. I have released a new version of the plugin (v1.0.19) that should resolve the following issues: mock.jest function, named exports with object pattern and fast-json-patch. Please let me know if it resolves your issues. I will look into the issue with type exports causing resets for the next plugin version.

pkratz commented 1 month ago

@FogelAI, thank you for the quick resolution. v1.0.19 resolves mocks, named exports with object pattern and fast-json-patch! I look forward to your next patch with the type export causing a reset.

FogelAI commented 1 month ago

@pkratz I have made a new commit. I haven't published a new version yet, but you can copy it manually from Github and try it. Please let me know if this resolves the type export issue.

pkratz commented 1 month ago

@FogelAI, that worked beautifully on resolving the type export issue!

However, now I'm running into some additional issues. These are unrelated to the new commit, but new issues I was able to encounter when I tried using the latest version of the plugin in my much larger project that has many 3rd party dependencies. I will try to get the two examples of issues that I'm running into into the smaller github repo I set up for barrel import testing, but I figured I'd let you know about them now in the event you get to play around with it before I get a chance to set up a more controlled example.

I have a barrel index for a package of ours that is effectively our redux package that is re-exporting redux, redux-undo and some other redux related libraries. The re-export of redux-undo, which we do via, *export from "redux-undo"**, is causing me issues when using the barrels plugin.

When the plugin is processing that export all statement, I run into this error:

C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:200
        barrelAST.program.body.every((node) => {
                  ^
TypeError: C:\apps\projects\git\Nextgen\client\packages\redux-experience\src\lib\data\dataSlice.ts: Cannot read properties of null (reading 'program')
    at BarrelFile.createSpecifiersMapping (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:200:19)
    at BarrelFile.handleExportAllDeclaration (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:179:25)
    at C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:217:18
    at Array.every (<anonymous>)
    at BarrelFile.createSpecifiersMapping (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:200:32)
    at BarrelFilesPackage.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:278:24)
    at Function.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:351:43)
    at PluginPass.call (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\main.js:19:46)
    at call (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\visitors.ts:303:14)
    at NodePath.call [as _call] (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:36:20)
    at NodePath.call (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:21:18)
    at NodePath.visit (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:95:31)
    at TraversalContext.visitQueue (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:147:16)
    at TraversalContext.visitMultiple (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:98:17)
    at TraversalContext.visit (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:177:19)
    at traverseNode (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\traverse-node.ts:40:17)
    at NodePath.visit (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:102:33)
    at TraversalContext.visitQueue (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:147:16)
    at TraversalContext.visitSingle (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:108:19)
    at TraversalContext.visit (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\context.ts:179:19)
    at traverseNode (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\traverse-node.ts:40:17)
    at traverse (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\index.ts:83:15)
    at transformFile (C:\apps\projects\git\Nextgen\client\node_modules\@babel\core\lib\transformation\index.js:105:29)
    at transformFile.next (<anonymous>)
    at run (C:\apps\projects\git\Nextgen\client\node_modules\@babel\core\lib\transformation\index.js:33:12)

In another test case in which I test a component that uses 'react-virtualized' I ran into a different problem. I have the following import in the component being pulled in from the test:

import { AutoSizer, List, ListRowProps } from "react-virtualized";

When that statement gets processed, I encounter the following error:

C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:24
    const localName = type !=="namespace" && specifierObj.localName.replaceAll(specifierObj.exportedName, "${specifier}");
                                                                    ^
TypeError: C:\apps\projects\git\Nextgen\client\packages\core-ui\src\lib\Picker\PickerList.tsx: Cannot read properties of undefined (reading 'replaceAll')
    at DefaultPatternExport.getSpecifierPattern (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:24:69)
    at DefaultPatternExport.createDefaultPattern (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:30:35)
    at DefaultPatternExport.isMatchDefaultPattern (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:49:12)
    at C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:119:44
    at Array.forEach (<anonymous>)
    at BarrelFile.handleExportNamedDeclaration (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:98:27)
    at C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:211:18
    at Array.every (<anonymous>)
    at BarrelFile.createSpecifiersMapping (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:200:32)
    at BarrelFilesPackage.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:278:24)
    at Function.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:351:43)
    at BarrelFile.getDeepestDirectSpecifierObject (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:250:54)
    at C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:117:49
    at Array.forEach (<anonymous>)
    at BarrelFile.handleExportNamedDeclaration (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:98:27)
    at C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:211:18
    at Array.every (<anonymous>)
    at BarrelFile.createSpecifiersMapping (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:200:32)
    at BarrelFilesPackage.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:278:24)
    at Function.getBarrelFile (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\barrel.js:351:43)
    at PluginPass.call (C:\apps\projects\git\Nextgen\client\babel-plugin-transform-barrels\src\main.js:19:46)
    at call (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\visitors.ts:303:14)
    at NodePath.call [as _call] (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:36:20)
    at NodePath.call (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:21:18)
    at NodePath.visit (C:\apps\projects\git\Nextgen\client\node_modules\@babel\traverse\src\path\context.ts:95:31)

Please let me know if you need me to set this up in a more controlled example, or if this is enough for you to debug further. Thanks!

pkratz commented 4 weeks ago

@FogelAI, I pushed a commit to https://github.com/pkratz/barrel-example to replicate the redux-undo and react-virtualized originated errors that I'm running into on my larger scale project with the babel-plugin-transform-barrels plugin.

You can replicate the errors I'm encountering by running:

node "node_modules/jest/bin/jest.js" "./apps/barrel-example/src/app/App.spec.tsx" -c "./apps/barrel-example/jest.config.ts" --no-cache

The following are the attached logs from the run.

redux_undo.log

This is the relevant error:

   TypeError: C:\apps\projects\git\sandbox\barrel-plugin\apps\barrel-example\src\app\app.tsx: Cannot read properties of null (reading 'program')

      198 |     createSpecifiersMapping(forceFullScan = false) {
      199 |         const barrelAST = AST.filenameToAST(this.path);
    > 200 |         barrelAST.program.body.every((node) => {
          |                   ^
      201 |           if (t.isExportNamedDeclaration(node)) {
      202 |             // export { abc } from './abc';
      203 |             // export { abc };

      at BarrelFile.createSpecifiersMapping (../../babel-plugin-transform-barrels/src/barrel.js:200:19)
      at BarrelFile.handleExportAllDeclaration (../../babel-plugin-transform-barrels/src/barrel.js:179:25)
      at ../../babel-plugin-transform-barrels/src/barrel.js:217:18
          at Array.every (<anonymous>)
      at BarrelFile.createSpecifiersMapping (../../babel-plugin-transform-barrels/src/barrel.js:200:32)
      at BarrelFilesPackage.getBarrelFile (../../babel-plugin-transform-barrels/src/barrel.js:278:24)

The react-virtualized originated error is reproducible by running:

node "node_modules/jest/bin/jest.js" "./apps/barrel-example/src/app/VirtualizedTester.spec.tsx" -c "./apps/barrel-example/jest.config.ts" --no-cache

The following are the logs from that run:

react_virtualized.log

The relevant error is:

    TypeError: C:\apps\projects\git\sandbox\barrel-plugin\apps\barrel-example\src\app\VirtualizedTester.tsx: Cannot read properties of undefined (reading 'replaceAll')

      22 |     const esmPath = this.getEsmPathPattern(specifierObj.esmPath, specifierObj.exportedName);
      23 |     const type = specifierObj.type;
    > 24 |     const localName = type !=="namespace" && specifierObj.localName.replaceAll(specifierObj.exportedName, "${specifier}");
         |                                                                     ^
      25 |     const exportedName = specifierObj.exportedName.replaceAll(specifierObj.exportedName, "${specifier}");
      26 |     return { esmPath, type, localName, exportedName };
      27 |   }

      at DefaultPatternExport.getSpecifierPattern (../../babel-plugin-transform-barrels/src/barrel.js:24:69)
      at DefaultPatternExport.createDefaultPattern (../../babel-plugin-transform-barrels/src/barrel.js:30:35)
      at DefaultPatternExport.isMatchDefaultPattern (../../babel-plugin-transform-barrels/src/barrel.js:49:12)
      at ../../babel-plugin-transform-barrels/src/barrel.js:119:44
          at Array.forEach (<anonymous>)
      at BarrelFile.handleExportNamedDeclaration (../../babel-plugin-transform-barrels/src/barrel.js:98:27)
      at ../../babel-plugin-transform-barrels/src/barrel.js:211:18
          at Array.every (<anonymous>)
      at BarrelFile.createSpecifiersMapping (../../babel-plugin-transform-barrels/src/barrel.js:200:32)
      at BarrelFilesPackage.getBarrelFile (../../babel-plugin-transform-barrels/src/barrel.js:278:24)
      at Function.getBarrelFile (../../babel-plugin-transform-barrels/src/barrel.js:351:43)
      at BarrelFile.getDeepestDirectSpecifierObject (../../babel-plugin-transform-barrels/src/barrel.js:250:54)
      at ../../babel-plugin-transform-barrels/src/barrel.js:117:49
          at Array.forEach (<anonymous>)
      at BarrelFile.handleExportNamedDeclaration (../../babel-plugin-transform-barrels/src/barrel.js:98:27)
      at ../../babel-plugin-transform-barrels/src/barrel.js:211:18
          at Array.every (<anonymous>)
FogelAI commented 2 weeks ago

@pkratz Thank you again for your detailed explanation. I have released a new version of the plugin (v1.0.20) that should resolve the following issues: type exports causing resets, react-virtualized and redux-undo. Please let me know if it resolves your issues.