Lazy loading fontawesome icons in vue3 + vite not working in DEV - vuejs3

In my vue3+vite project I'm using the official fontawesome vue3 package (see use with vue).
In order to enable tree-shaking you need to statically load the necessary icons (or possibly all of them) in advance using library.add. See for instance the following App.vue
<script setup>
import { ref, computed } from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
import { definition } from "#fortawesome/free-solid-svg-icons/faTruck";
library.add(definition);
const icon = ref("");
const showIcon = () => { icon.value = `fa-solid fa-truck`; };
</script>
<template>
<button #click="showIcon">Show Truck Icon</button>
<div v-if="icon">
<font-awesome-icon :icon="icon" />
</div>
</template>
here we statically load the truck icon and when you click the button the icon shows up.
What I was trying to do is loading the icons on demand (in this case, only when the button is clicked), using the following code:
<script setup>
import { ref, computed } from "vue";
import { library } from "#fortawesome/fontawesome-svg-core";
import { FontAwesomeIcon } from "#fortawesome/vue-fontawesome";
const modules = import.meta.glob(
"../node_modules/#fortawesome/free-solid-svg-icons/faTruck.js",
{ eager: false, import: "definition" }
);
const icon = ref("");
const showIcon = () => {
Object.values(modules)[0]().then((elem) => {
library.add(elem);
icon.value = `fa-solid fa-truck`;
});
};
</script>
<template>
<button #click="showIcon">Show Truck Icon</button>
<div v-if="icon">
<font-awesome-icon :icon="icon" />
</div>
</template>
But this doesn't work in "develpment" (npm run dev):
it makes a call to http://localhost:5173/node_modules/#fortawesome/free-solid-svg-icons/faTruck.js
then raises an error: Uncaught (in promise) ReferenceError: exports is not defined
while it works fine when the bundle is built (npm run build then for example serve the dist folder with http-server)
I suspect the problem is related to the fact that in development mode faTruck.js module is used "as is", while it is transpiled in the build phase.
Is there a solution?
NOTE:
The example contains only the "truck" because is over-simplified, but actually any icon should be loaded; i.e. the actual path in import.meta.glob should be ../node_modules/#fortawesome/free-solid-svg-icons/fa*.js
Full steps to reproduce the issue:
npm create vue#3 # accepts all defaults
cd vue-project
npm i #fortawesome/fontawesome-svg-core #fortawesome/free-solid-svg-icons #fortawesome/vue-fontawesome
# replace src/App.vue with the one indicated above
# run in dev with
npm run dev
# or build for prod and then expose using http-server
npm run build
npx http-server dist

Explaination
According to the Vite pre-bundling docs:
Vite's dev serves all code as native ESM. Therefore, Vite must convert dependencies that are shipped as CommonJS or UMD into ESM first
But when you use glob import with dynamic variables, your modules will not be pre-bundled. Since #fortawesome/free-solid-svg-icons/faTruck.js is a CommonJS file, it can not be used directly in ESM. And you are right that Vite does transform the module on production build, so it works well on production.
You may think about the optimizeDeps.include option but unfortunately, it does not help in this situation. Even if you add your module to the include list, Vite does pre-bundle your module but it will not use that pre-bundled file for your dynamic import. It still uses the file in node_modules/#fortawesome/free-solid-svg-icons/ folder.
I'm afraid that there is no straightforward solution to your problem. See this issue
Workaround
Just make it work differently on dev and prod.
const showIcon = async () => {
let x = 'faTruck'
let definition
if (import.meta.env.PROD) {
const iconModule = await import(
`../node_modules/#fortawesome/free-solid-svg-icons/${x}.js`
)
definition = iconModule.definition
} else {
const iconModule = await import(`#fortawesome/free-solid-svg-icons`)
definition = iconModule[x]
}
library.add(definition)
icon.value = `fa-solid fa-truck`
}
With this code, you still have the benefit of lazy loading on production and a smooth dev server to work
Another approach
Hard-coding your import list like so:
const showIcon = async (iconName) => {
const listImport = {
faTruck: () => import(`#fortawesome/free-solid-svg-icons/faTruck`),
faWarning: () => import(`#fortawesome/free-solid-svg-icons/faWarning`),
}
const iconModule = await listImport[iconName]()
console.log('iconModule', iconModule)
library.add(iconModule.definition)
}
But I bet you have hundreds of icons in your list so it hardly is an option

Related

vue & vitest, data-v-[random-number] attribute being added to html

Introduction: I am working with vite and vitest, I am doing snapshot tests for components that are 100% template and do not contain any logic
Problem: data-v-[random-number] It is being added to the root element of each component and snapshots are always different
What i want: Understand why im getting this data-v-[random-number] and if possible, a way to avoid this problem
Short example code:
BaseText.vue:
<script setup lang="ts"></script>
<template>
<span><slot></slot></span>
</template>
BaseText.spec.ts:
import { describe, it, expect } from "vitest";
import { shallowMount } from "#vue/test-utils";
import BaseText from "./BaseText.vue";
describe(name, () => {
const wrapper = shallowMount(BaseText);
it("MatchSnapshot", () => {
expect(wrapper.html()).toMatchSnapshot();
});
});
Error when runing tests:
- Expected ""<span data-v-25e5131c=""></span>""
+ Received ""<span data-v-b3462088=""></span>""

Next js and Next Auth overlapping react declarations

I am running Next js and Next Auth in multiple project, and all of a sudden all of them decided to crash with the same error.
Module parse failed: Identifier '_react' has already been declared (14:6)
File was processed with these loaders:
* ./node_modules/next/dist/build/webpack/loaders/next-swc-loader.js
You may need an additional loader to handle the result of these loaders.
| const _material = require("#mui/material");
| const _xDataGrid = require("#mui/x-data-grid");
> const _react = require("next-auth/react");
| const _reportTable = /*#__PURE__*/
a simple example that crashes looks like this...
As you can see from the example below. I am not importing react twice.
import React from "react";
import { Box } from "#mui/material";
import { DataGrid, GridColDef, GridRowsProp } from "#mui/x-data-grid";
import { getSession } from "next-auth/react";
import ReportTable from "../src/components/ReportTable";
export default function Home() {
const findSession = () => {
const session = getSession();
console.log(session);
return session;
};
return (
<Box>
<ReportTable title="Price Books">
<DataGrid
sx={{ border: "0" }}
rows={rows}
columns={columns}
headerHeight={40}
/>
</ReportTable>
</Box>
);
}
If I remove the getSession import at the top everything runs fine. The other developers on my team can run these project just fine, so I believe it's an environmental issue on my side.
Has anyone else run into this issue?
I have built the project and it works fine. The errors only occur in my dev environment.
I also cloned the repo on my personal machine and it worked fine there as well.
The problem was with a new plugin that came out today, "Code Ninja". If you are facing this issue, disable that VSCode extension.

Multiple conflicting contents for sourcemap source error when using client-only

I have a component that uses windows so I rendered it with client-only like:
<template>
<h2>Testing Component2</h2>
<client-only>
<Component2></Component2>
</client-only>
</template>
<script lang="ts">
import Component2 from './component-2'
export default defineComponent({
components: {
Component2
},
setup() {}
})
</script>
this works locally, the problem is that when I try to build to deploy, it always returns the error:
Multiple conflicting contents for sourcemap source error ./src/pages/index.vue
Is there a way to fix this or am I doing something wrong?
I used
LazyClientOnly
and there add a lazy import for the component like: Component2: () => import('./component-2') and works perfectly

Vue 3. Vite, createApp() and include CSS from single file components within iframe

Bit of a tricky one and a unique use case.
I have a Vue 3 app, developing and building using Vite.
i am instantiating a second Vue 3 app instance inside an iframe, relevant code pasted below. As i need unpolluted components and styles available for the second app in the iframe.
import { h, ref, createApp, onMounted, onBeforeUnmount, onBeforeUpdate } from "vue"
export default {
setup(props, { attrs, slots, emit, expose }) {
const iframeRef = ref(null)
const iframeBody = ref(null)
const iframeHead = ref(null)
const iframeStyle = ref(null)
let app = null
let emitSelection;
onMounted(() => {
iframeBody.value = iframeRef.value.contentDocument.body
iframeHead.value = iframeRef.value.contentDocument.head
const el = document.createElement("div")
iframeBody.value.appendChild(el)
app = createApp(MySecondAppLayout, props)
app.use(MyUILibrary);
app.mount(el)
})
return () => h("iframe", { ref: iframeRef })
}
Everything is working great, the second app instance imports it's own components and mount and behave correctly within the iframe. However, the SCSS/CSS styles are not included within the iframe context.
Is what i am attempting to do impossible? Or is there a tag i can inject into the iframe to include the styles from the imported components?
Alternatively should i/can i get vite output the style sheets from all of the single file components as a text file so i can paste it in manually at the top of the iframe?
I was able to repackage my component library using vite, and then using this plugin: https://www.npmjs.com/package/vite-plugin-libcss i could import the css manually into the iframe.

Cannont mount twice the same Vue 3 component

Im' mounting a Vue 3 component as a loading animation during a frame loading with :
createApp(App).mount(e);
When the frame is loaded, the component is erased in the html by the frame content (but not by Vue I guess). This behaviour is managed by an external system of Vue.
When I'm trying to reload the component with the same command, the component is not displayed.
If I only mount again the component, I have the following warning in the console and the component is not displayed :
"[Vue warn]: App has already been mounted.
If you want to remount the same app, move your app creation logic into a factory function and create fresh app instances for each mount - e.g. `const createMyApp = () => createApp(App)`".
I've found also a way to replicate the issue with :
const app = createApp(App)
app.mount(el);
el.innerHtml = '';
app.mount(el);
I also try to unmount the component with no more success :
const app = createApp(App);
app.mount(el);
app.unmount();
app.mount(el);
So what is the correct way to display again the same vue component when it is erased externally ?
To mount/unmount 2 instances of Vue, it is necessary to use render method.
So, for exemple below, this solution works :
var app = createApp({ render: () => h(App) })
app.mount(el);
el.innerHtml = '';
app = createApp({ render: () => h(App) })
app.mount(el);
Don't forget to import h() function with :
import { createApp, h } from 'vue';
An far more better option is to use Vue custom elements.
First, create a classical SFC with ce.vue extension :
//test.ce.vue
<template>
<div class="text-primary">Test</div>
</template>
<script>
export default {
name: 'test',
};
</script>
<style>
.text-primary {
color: red;
}
</style>
And then in the main script :
//app.js
import Test from 'test.ce.vue';
const testElement = defineCustomElement(Test);
customElements.define('test-element', testElement);
document.body.appendChild(document.createElement('test-element'));
The component will be rendered as soon as is detected in the document:
<test-component>
#shadow-root (open)
<style>
.text-primary {
color: red;
}
</style>
<div class="text-primary">Test</div>
</test-component>

Resources