vuejs / vue-jest

Jest Vue transformer
MIT License
748 stars 156 forks source link

`vue3-jest` - Imports from directories and packages with namespace 'vue' get cut out when running unit tests. #512

Open Giergiel96 opened 2 years ago

Giergiel96 commented 2 years ago

After migrating to vue 3, and vue-jest@29.2.0 some tests stopped working for us. After some digging we found out that if you have a package named like "@some-ui-kit/vue", named imports from it will get cut from the component's scope durin test runtime. Changing the import path to anything different changes the issue, but it used to work perfectly fine with Vue 2 compatible versions.

As you can see, the example "TEST" const disappears from the scope: image image

But if you rename the "package", then it works: image image

Reproduction repo: https://github.com/Giergiel96/repro-vue-jest.git

Giergiel96 commented 1 year ago

After some more digging i found out, that the issue is that the "_vue" variable holding the scope with the imports get overwritten by the template script. This is the code generated by the vue-jest after adding scriptResult, scriptSetupResult and templateResult (generate-code.js): image

lmiller1990 commented 1 year ago

Wait - so the bug is only if your package is named vue?

Not sure how best to fix this one. I guess our code gen isn't ideal.

Giergiel96 commented 1 year ago

This one's weird. It looks like babel creates a namespace out of the last part of the import path (i.e. @random-package/vue or @package/components/vue). When you import anything other from anything that will result in creation of the _vue namespace, then the problem disappears since it will autoincrement and create _vue1, _vue2 starting from the number 1 🤷 What we did for now was to write a babel plugin that runs only during tests, that looks for any _vue variable declaration and does not import from vue directly and "increments" the name to _vue1 so it does not get overwritten by the impots from template code 😂

Giergiel96 commented 1 year ago

Example of that is in the reproduction repo.

lmiller1990 commented 1 year ago

Weird one... I am not sure I have time to fix this, if you want to try, that'd be great. Alternatively - just rename your package to not be named vue. Not ideal, but an easy work around in the mean time.

mrgodhani commented 7 months ago

@Giergiel96 do you have an example of the babel plugin and how you solved it?

paulkirow commented 2 months ago

I just stumbled onto this issue after debugging a similar issue for a while.

I have an inertiajs vue project I'm upgrading to vue 3.

Inertiajs imports look like this:

import { useForm } from '@inertiajs/vue3';

with @inertiajs/vue3 being resolved to _vue via generateUidIdentifier

Here's the an example of transpired code:

(redacted to show relevant bits)

>>scriptResult START
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.default = void 0;
var _vue = require("@inertiajs/vue3");
var _vue2 = require("vue");
var _default = exports.default = {
  layout: _LayoutGrey,
  props: ['targetedmessages', 'sort', 'dir', 'statuses', 'audiences', 'advancedFiltersCount'],
  components: {
    ...
  },
  data() {
    return {
      filters: (0, _vue.useForm)({
        basic: {
          ...
        },
        advanced: {
          ...
        }
      })
    };
  }
};
>> scriptResult END
>> tempalteResult START
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.render = render;
var _vue = require("vue");
var _hoisted_1 = {
  id: "mainContent",
  tabindex: "-1",
  "class": "text-xl"
};
...

generateUidIdentifier works well to increment _vue in the scriptResult part, however when generating tempalteResult I guess we lose the context that the _vue identifier is already in use.

I'm going to look into @Giergiel96's work around.

But anyone using jest, vue and inertiajs is likely going to run into this problem.

paulkirow commented 2 months ago

I found my own workaround for this issue.

babel-plugin-inject-vue.js

/**
 * This Babel plugin fixes an issue in vue3-jest related to how imports are handled
 * in Vue files. The problem arises from a combination of factors:
 *
 * In JavaScript, imports like `import { useForm } from '@inertiajs/vue3';` are transformed
 * into lines like `_vue = require("@inertiajs/vue3");`. The `_vue` name is generated by
 * Babel's `generateUidIdentifier` method, which converts "@inertiajs/vue3" to "vue3", removes
 * numbers, and adds an underscore, resulting in `_vue`.
 *
 * Normally, Babel increments `_vue` to `_vue2` if there are duplicate identifiers, preventing
 * conflicts. However, in vue3-jest, the template and script parts of Vue files are processed
 * separately, so the increment is lost. This can lead to both `_vue = require("@inertiajs/vue3");`
 * and `_vue = require("vue");` in the same file, with one potentially overwriting the other.
 *
 * This plugin works around the issue by injecting `import { render } from 'vue';` ("render" is arbitrary) at the top
 * of any Vue file before any transpiling occurs. This ensures that `_vue = require("vue");`
 * is always declared first, so any other imports resolving to `_vue` will be incremented
 * first to `_vue2`, preventing overwrites.
 */
module.exports = function ({ types: t }) {
    return {
        visitor: {
            Program(path, state) {
                // Only process .vue files
                const filename = state.filename || '';
                if (!filename.endsWith('.vue')) return;
                // Check if the file has a <script> block
                if (path.node.body.length > 0) {
                    // Add an import statment for vue (equivalent to `import { render } from 'vue'`)
                    const vueImport = t.importDeclaration(
                        [t.importSpecifier(t.identifier('render'), t.identifier('render'))],
                        t.stringLiteral('vue')
                    );
                    path.unshiftContainer('body', vueImport);
                }
            },
        },
    };
};

babel.config.js

module.exports = {
    env: {
        test: {
            ...
            plugins: [
                ...
                './babel-plugin-inject-vue'
            ],
        },
    },
};

I'd love to contribute some fix here but I'm not competent in babel to understand what the correct fix would be.