Using vue slots in library gives currentRenderingInstance is null - vue.js

I am creating a Vue component library with Rollup, but when I use slots it gives me the following error:
Uncaught (in promise) TypeError: currentRenderingInstance is null
I made a very simple component in my library:
<script setup></script>
<template>
<button>
<slot></slot>
</button>
</template>
<style scoped></style>
Then I simply use it like this:
<ExampleComponent>
Text
</ExampleComponent>
If I remove the slot and replace it with a prop or hard-coded text, everything works fine.
This is my rollup.config.js:
import { defineConfig } from 'rollup';
import path from 'path';
import resolve from '#rollup/plugin-node-resolve';
import commonjs from '#rollup/plugin-commonjs';
import postcss from 'rollup-plugin-postcss';
import vue from 'rollup-plugin-vue';
// the base configuration
const baseConfig = {
input: 'src/entry.js',
};
// plugins
const plugins = [
vue(),
resolve({
extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'],
}),
// process only `<style module>` blocks.
postcss({
modules: {
generateScopedName: '[local]___[hash:base64:5]',
},
include: /&module=.*\.css$/,
}),
// process all `<style>` blocks except `<style module>`.
postcss({ include: /(?<!&module=.*)\.css$/ }),
commonjs(),
];
const external = ['vue'];
const globals = {
vue: 'Vue',
};
export default [
// esm
defineConfig({
...baseConfig,
input: 'src/entry.esm.js',
external,
output: {
file: 'dist/vue-my-lib.esm.js',
format: 'esm',
exports: 'named',
},
plugins,
}),
// cjs
defineConfig({
...baseConfig,
external,
output: {
compact: true,
file: 'dist/vue-my-lib.ssr.js',
format: 'cjs',
name: 'VueMyLib',
exports: 'auto',
globals,
},
plugins,
}),
// iife
defineConfig({
...baseConfig,
external,
output: {
compact: true,
file: 'dist/vue-my-lib.min.js',
format: 'iife',
name: 'VueMyLib',
exports: 'auto',
globals,
},
plugins,
}),
];
Any idea about the problem?

After a whole day of searching, I found the solution (here and here). It's a problem with using a library locally (e.g., through npm link) where it seems there are two instances of Vue at the same time (one of the project and one of the library). So, the solution is to tell the project to use specifically its own vue through webpack.
In my case, I use Jetstream + Inertia, so I edited webpack.mix.js:
const path = require('path');
// ...
mix.webpackConfig({
resolve: {
symlinks: false,
alias: {
vue: path.resolve("./node_modules/vue"),
},
},
});
Or if you used vue-cli to create your project, edit the vue.config.js:
const { defineConfig } = require("#vue/cli-service");
const path = require("path");
module.exports = defineConfig({
// ...
chainWebpack(config) {
config.resolve.symlinks(false);
config.resolve.alias.set("vue", path.resolve("./node_modules/vue"));
},
});

Thanks to #mikelplhts
On vite + esbuild I used:
export default defineConfig({
...
resolve: {
alias: [
...
{
find: 'vue',
replacement: path.resolve("./node_modules/vue"),
},
],
},
...

Related

How to use Environment Variables inside Vue3+Vite component library?

I have created a component as part of my component library that I am building with Vue3 and Vite. Everything works well, except when I try to use environment variables. I want the app that consumes this component library to be able to provide the component with environment specific data.
I have played around and found that if I have a .env file as part of the component library project, I am able to access those variables, but I want to be able to provide that during runtime and not during build time.
Here is my vite.config.ts
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "#vitejs/plugin-vue";
import dts from "vite-plugin-dts";
export default ({ mode }) => {
return defineConfig({
optimizeDeps: {
exclude: ["vue-demi"],
},
plugins: [
vue(),
dts({
insertTypesEntry: true,
}),
],
server: {
open: true,
},
build: {
lib: {
entry: resolve(__dirname, "src/lib.ts"),
name: "complib",
fileName: "complib",
},
rollupOptions: {
external: ["vue"],
output: {
globals: {
vue: "Vue",
},
exports: "named",
},
},
},
});
};
The entry looks like:
import { App, install } from "vue-demi";
import TestComp from "./components/TestComp.vue";
import "./tailwind.css";
install();
export default {
install: (app: App) => {
app.component("TestComp", TestComp);
},
};
export { Header };
And here is a minimal component TestComp.vue:
<script setup lang="ts">
import { onMounted } from "vue";
onMounted(() => {
console.log(import.meta.env.VITE_TEST_VAR);
});
</script>
<template>
<span>Test Comp</span>
</template>

How to mock SVG's when snapshot testing with vitest and Storybook?

I am trying to run vitest snapshot tests on Storybook stories using the composeStories Fn from #storybook/testing-react, but I keep getting the error:
FAIL src/components/common/Nav/Nav.test.tsx > Nav Component > it should match the snapshot
Error: Element type is invalid: expected a string (for built-in components) or a class/function
(for composite components) but got: undefined. You likely forgot to export your component from
the file it's defined in, or you might have mixed up default and named imports.
Check the render method of `Nav`.
//... stack trace
I believe it's related to the svg imports, as this only occurs in components that import svgs as react components via the SVGR library. i.e.
// components/common/Nav.tsx
import { ReactComponent as ECDLogo } from '#assets/ecd_logo.svg';
And my vite.config.ts uses the vite-svgr-plugin:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '#vitejs/plugin-react';
import svgr from 'vite-plugin-svgr';
import tsconfigPaths from 'vite-tsconfig-paths';
import path from 'path';
const tsConfigPathsOpts = {
extensions: ['.svg', '.png', '.jpeg'],
loose: true,
};
export default defineConfig({
build: {
outDir: 'build',
},
define: {
global: {},
},
resolve: {
alias: {
'#': path.resolve(__dirname, './src'),
'#assets': path.resolve(__dirname, './src/assets'),
'#styles': path.resolve(__dirname, './src/styles'),
'#types': path.resolve(__dirname, './src/types'),
'#components': path.resolve(__dirname, './src/components'),
},
},
plugins: [react(), svgr(), tsconfigPaths(tsConfigPathsOpts)],
});
My Storybook config (.storybook/main.js) looks like so:
const path = require('path');
const { mergeConfig } = require('vite');
const tsconfigPaths = require('vite-tsconfig-paths');
const svgr = require('vite-plugin-svgr');
const tsConfigPathsOpts = {
extensions: ['.svg', '.png', '.jpeg'],
loose: true,
};
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.#(js|jsx|ts|tsx)'],
addons: [
'#storybook/addon-essentials',
'#storybook/preset-create-react-app',
'#storybook/addon-a11y',
'#storybook/node-logger',
'storybook-addon-designs',
'storybook-color-picker',
'storybook-dark-mode',
],
framework: '#storybook/react',
core: {
builder: '#storybook/builder-vite',
},
async viteFinal(config, { configType }) {
return mergeConfig(config, {
resolve: {
alias: {
'#': path.resolve(__dirname, '../src'),
'#assets': path.resolve(__dirname, '../src/assets'),
'#styles': path.resolve(__dirname, '../src/styles'),
'#types': path.resolve(__dirname, '../src/types'),
'#components': path.resolve(__dirname, '../src/components'),
},
},
plugins: [svgr(), tsconfigPaths.default(tsConfigPathsOpts)],
});
},
};
I've come to understand that I need to mock these SVG's so that their snapshot is consistent, but I need direction on whether my mocking implementation is correct. See the vi.mock Fn below.
// components/common/Nav/Nav.test.tsx
import React from 'react';
import { render } from '#testing-library/react';
import '#testing-library/jest-dom/extend-expect';
import { composeStories } from '#storybook/testing-react';
import * as stories from './Nav.stories'; // import all stories from the stories file
import { vi } from 'vitest';
const { NavDefault } = composeStories(stories);
👀
vi.mock('#assets/*', () => {
return {
default: 'SVGUrl',
ReactComponent: 'div',
};
});
describe('Nav Component', () => {
test('it should match the snapshot', () => {
const { asFragment } = render(<NavDefault />);
expect(asFragment()).toMatchSnapshot();
});
});
I was expecting this to mock all the imports from #assets/* to be strings "SVGUrl" or 'div'
But I get the same error as above:
FAIL src/components/common/Nav/Nav.test.tsx > Nav Component > it should match the snapshot
Error: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.
we had the same issue today and fixed it by adding the plugin svgr() in vitest.config.ts
import svgr from "vite-plugin-svgr";
export default defineConfig({
plugins: [
// ...other plugins
svgr(),
]
})

Storybook/Vue: style not applied

I installed storybook via the vue-cli and added a .scss file in my preview.js:
import "assets/scss/core.scss";
import Vue from "vue";
import CompositionApi from "#vue/composition-api";
Vue.use(CompositionApi);
where "assets" is an alias configured in my vue.config.js to refer to the src/assets path. According to the docs :
The webpack config used for storybook is resolved from vue-cli-service, which means you don't need to have any special webpack section in storybook config folder.
So the path should ne correctly resolved, right ? However the style does not seem to be loaded as it is not applied to my components.
Am I missing something ?
FYI, here is my main.js:
module.exports = {
core: {
builder: "webpack5",
},
stories: ["../../src/**/*.stories.#(js|jsx|ts|tsx|mdx)"],
addons: [
"#storybook/addon-essentials",
"#storybook/addon-links",
],
};
And my vue.config.js :
const path = require("path");
module.exports = {
configureWebpack: {
resolve: {
alias: {
//aliases go here
"#": path.resolve(__dirname, "src"),
assets: path.resolve(__dirname, "./src/assets"),
},
},
},
css: {
loaderOptions: {
scss: {
additionalData: `#import 'assets/scss/variables';`,
},
},
},
};
Thanks !

Multiple entry points in Vite

I have a Vue2 project with Webpack, and I'm trying to switch from Webpack to Vite.
In webpack.common.js, I have multiple entry points:
module.exports = {
entry: {
appSchool: './resources/school/app.js',
appStudent: './resources/student/app.js',
appAuth: './resources/auth/app.js'
},
...
}
How do I write this in vite.config.js?
Vite uses Rollup under the hood, and you can configure Rollup through build.rollupOptions, and then Rollup's input option:
// vite.config.js
import { fileURLToPath } from 'url'
import { defineConfig } from 'vite'
import vue from '#vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
input: {
appSchoool: fileURLToPath(new URL('./resources/school/index.html', import.meta.url)),
appStudent: fileURLToPath(new URL('./resources/student/index.html', import.meta.url)),
appAuth: fileURLToPath(new URL('./resources/auth/index.html', import.meta.url)),
},
},
},
})
Note the entry points refer to index.html files, which themselves link to the app.js in their corresponding directories (e.g., ./resources/student/index.html contains <script src="./app.js">). The input config also accepts the app.js file directly, but no HTML would be generated.
demo

Wrong import path after building vue proect as a "Library"

I am currently coding a small vue component library for company's internal use. However, I faced some difficulties on building and deploy the project. This component library contains several components inside. And it will finally build by vue-cli-service build --target lib. The problem I am facing right now is that I have a component (i.e. called Audio.vue), and it will import a .mp3 file inside the component. The component is look like this
<template>
<audio controls :src="soundSrc" />
</template>
<script>
import { defineComponent } from 'vue';
import sound from './assets/sound.mp3';
export default defineComponent({
name: 'Audio',
props: {},
setup() {
return { soundSrc: sound };
},
});
</script>
<style scoped></style>
However, I use this component by serving (vue-cli-service serve") my project is fine. But if I build this project by running vue-cli-service build --target lib --name project-name-here. And I use this built version as a git submodule of my library in another vue project by importing project-name-here.common built before. The sound.mp3 import from './assets/sound.mp3' could not be found. It seems it is using a relative path of my another vue project (i.e. localhost:8080) instead of the library project
Here is my vue.config.js
const path = require('path');
module.exports = {
css: {
extract: false,
},
lintOnSave: false,
productionSourceMap: false,
configureWebpack: {
resolve: {
alias: {
mediaAsset: path.resolve(__dirname, './src/components/Audio/assets'),
},
},
},
chainWebpack: (config) => {
const imgRule = config.module.rule('images');
imgRule
.use('url-loader')
.loader('url-loader')
.tap((options) => Object.assign(options, { limit: Infinity }));
const svgRule = config.module.rule('svg');
svgRule.uses.clear();
svgRule
.test(/\.svg$/)
.use('svg-url-loader') // npm install --save-dev svg-url-loader
.loader('svg-url-loader');
config.module
.rule('raw')
.test(/\.txt$/)
.use('raw-loader')
.loader('raw-loader');
config.module
.rule('media')
.test(/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/)
.use('url-loader')
.loader('url-loader')
.tap((options) =>
Object.assign(options, {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: 'media/[name].[hash:8].[ext]',
},
},
})
);
},
};
Appreciated for answering this question. Thanks.