vuejs how to compile single file component to js object - vue.js

I have a simple Vue js single file component:
<template>
<div>
Hello!
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
And I want to compile it to
{
template: '<div>Hello!</div>'
}
of course, it should work with more complex examples, with included components and such.
How do I do that?

You can use vue-template-compiler package:
Install it:
npm i vue-template-compiler
Use it (test.vue is a component from your example):
const fs = require('fs');
const compiler = require('vue-template-compiler');
const content = fs.readFileSync('./test.vue', 'utf-8');
const res = compiler.parseComponent(content);
const template = res.template.content.trim();
const result = { template };
console.log(result);
Result:
{ template: '<div>\n Hello!\n</div>' }
Don't forget to add error checks to your code.

Related

ReferenceError: computed is not defined on Vitest test suite

Description
I'm migrating test suites from Jest to Vitest.
But i've a problem when i run test suites, an error occurs when a component has a computed property.
The common error is :
ReferenceError: computed is not defined
- /components/Ui/Avatar.vue:13:30
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:157:22
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:7084:29
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:7039:11
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:5401:13
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:5376:17
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:4978:21
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:5515:21
- /node_modules/#vue/reactivity/dist/reactivity.cjs.js:189:25
- /node_modules/#vue/runtime-core/dist/runtime-core.cjs.js:5622:56
Versions
"vitest": "^0.18.1"
"jsdom": "^20.0.0"
"#vue/test-utils": "^2.0.2"
Exemple
Here is my component code :
<template>
<image
:src="src"
:onerror="onErrorLoadImage"
:class="['avatar', { big }]"
/>
</template>
<script setup lang="ts">
const props = withDefaults(defineProps<{
src?: string
big?: boolean
errorImage?: string
}>(), {
src: '',
big: false,
errorImage: '/no-avatar.png',
})
const onErrorLoadImage = computed(() => `this.src='${props.errorImage}';this.onerror='';`)
</script>
And my test
import { describe, it, expect } from 'vitest'
import { mount } from '#vue/test-utils'
import UiAvatar from './Avatar.vue'
const componentName = 'img'
const src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
const big = true
const errorImage = '/no-avatar.png'
describe('UiAvatar', () => {
it('should be render the component', () => {
const wrapper = mount(UiAvatar, {
propsData: {
src,
big,
errorImage
}
})
expect(wrapper.element.tagName).toBe(componentName)
})
})
Thanks :)
This can be solved by using the following npm packages:
unplugin-vue-components/vite
unplugin-auto-import/vite
They only need to specify a path where the import files will be generated, in my case I use a storybook folder since I also use the plugin there, but it can be any other path
vitest.config.ts

AG Grid: Framework component is missing the method getValue() in production build but not development

We have defined a checkbox editor for ag-grid like this:
<template>
<input type="checkbox" v-model="checked" />
</template>
<script setup lang="ts">
import type { ICellEditorParams } from "ag-grid-community";
import { ref } from "vue";
type Props = {
params: ICellEditorParams;
};
const props = defineProps<Props>();
const checked = ref(props.params.value);
const getValue = () => checked.value;
defineExpose({
getValue,
});
</script>
When running npm run dev we see the following build output. The ag-grid works. No problems. Good!
When running npm run build && npm run preview we see the following build output. The ag-grid complains about a missing getValue().
A previous question established a probably root cause:
ag-grid properly works with functions directly returned from setup() function and haven't access to functions returned via exposed context properties
So, how can we make our vite production build return the getValue in the same what that the development build does?
Until either Vite or ag-grid support using defineExpose(), this workaround returns getValue() to ag-grid by using <script> instead of <script setup>.
<template>
<input type="checkbox" v-model="checked" />
</template>
<script lang="ts">
import type { ICellEditorParams } from "ag-grid-community";
import { ref } from "vue";
type Props = {
params: ICellEditorParams;
};
export default {
setup(props: Props) {
const checked = ref(props.params.value?.valueOf());
console.log("wat", checked.value);
return {
checked,
getValue: () => checked.value,
};
},
};
</script>

How can I lazy load icons from Vue CoreUI?

I just ran npm run build on my vue 3 project and my vendor.js file turns out to be 10MB!!!
And when I briefly scan the code of that file, it seems to be mostly filled with svg code. So I'm assuming that that's all the icons from coreui. Because currently, my main.ts file has this code:
import { CIcon } from '#coreui/icons-vue'
import * as coreuiIcons from '#coreui/icons'
app.provide('icons', coreuiIcons)
app.component('CIcon', CIcon)
So I'm wondering if it's possible to let those icons load lazily, just whenever they're being called for in the templates.
So far, I've tried it like this:
// CIconWrapper.vue
<template>
<CIcon :icon="i" :size="size" />
</template>
<script lang="ts" setup>
import { CIcon } from "#coreui/icons-vue"
const { icon, size = "" } = defineProps<{ icon: string, size?: string }>()
const i = await import('#coreui/icons')[icon]
</script>
And then as fas as a I know, asynchronous Vue components only really work when wrapping them inside a Suspense, so I created another wrapper for the wrapper:
// CIconWrapperSuspense.vue
<template>
<Suspense>
<template #default>
<CIcon :icon="icon" :size="size" />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
</template>
<script lang="ts" setup>
import { Suspense } from "vue"
import { default as CIcon } from "./CIconWrapper.vue"
const { icon, size = "" } = defineProps<{ icon: string, size?: string }>()
</script>
And then I changed my code in main.ts to look like this:
import { default as CIcon } from '#/components/CIconWrapperSuspense.vue'
app.component('CIcon', CIcon)
But it doesn't do anything. Like literally nothing. No loading screen even.
So if you have any tips on how to make this actually work, I would be incredibly grateful. πŸ™
Okay I fixed it! :-D
So I threw away CIconWrapperSuspense.vue, that wasn't necessary at all.
Then I rewrote CIconWrapper.vue as follows:
<template>
<CIcon v-if="i" :content="i" :size="size" :customClasses="customClasses" />
</template>
<script lang="ts" setup>
import { onMounted } from "vue"
import { CIcon } from "#coreui/icons-vue"
let i = $ref()
const { icon, size = "", customClasses = "" } = defineProps<{
icon: string, size?: string, customClasses?: string|string[]|object
}>()
onMounted(async () => {
const iconName = icon.replace(/-(\w)/g, match => match[1].toUpperCase())
const fileName = icon.replace(/([a-z])([A-Z])/g, match => `${match[0]}-${match[1].toLowerCase()}`)
// Vite / Rollup only accept dynamic imports that start with ./ or ../
i = (await import(`../../node_modules/#coreui/icons/js/free/${fileName}.js`))[iconName]
})
</script>
And now my main.ts file has this code:
// icons
import CIcon from '#/components/CIconWrapper.vue'
app.component('CIcon', CIcon)

vite 2 production env ref element is undefined with compostion api

I use vue3 with composition api, but when I build my project, the ref element always undefined.
I reproduced it, maybe I used it incorrectly, but I don't know why.
I defined a ref in hooks function.
const isShow = ref(false)
const rootRef = ref<HTMLDivElement>();
export default function () {
function changeShow() {
isShow.value = !isShow.value;
console.log(isShow.value, rootRef.value);
}
return { isShow, rootRef, changeShow };
}
Use rootRef in the HelloWorld.vue and linked element.
<script setup lang="ts">
import useShow from "../composables/useShow";
const { rootRef, isShow } = useShow();
</script>
<template>
<div ref="rootRef" v-show="isShow" class="test"></div>
</template>
Create a button in App.vue and bind click function.
<script setup lang="ts">
import HelloWorld from "./components/HelloWorld.vue";
import useShow from "./composables/useShow";
const { changeShow } = useShow();
</script>
<template>
<button #click="changeShow">εˆ‡ζ’</button>
<HelloWorld />
</template>
When I click button, it works.
But when I build it and import from lib, it doesn't work.
My vite.config.ts is as follows:
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
"#": path.resolve(__dirname, "src")
}
},
build: {
cssCodeSplit: true,
sourcemap: true,
lib: {
entry: path.resolve(__dirname, "src/index.ts"),
name: "my-project",
fileName: format => `my-project.${format}.js`
},
rollupOptions: {
external: ["vue"],
preserveEntrySignatures: "strict",
output: {
globals: {
vue: "Vue"
}
}
}
}
});
I think the problem is the definition of rootRef. It seems that only binding location can use it. This is no different from defining it in a component. I need to use it in multiple places.
Oddly, in this way, the Dev environment works fine, but Pro env is not available. Do I need to modify the build configuration of vite.
How do I do that?
The problem is your App.vue uses its own copy of composables/useShow instead of the one from the lib.
The solution is to export the composable from the lib so that your app can use the same one:
// src/index.ts
import { default as useShow } from './composables/useShow';
//...
export default {
//...
useShow
};
In App.vue, use the lib's composable:
import MyProject from "../dist/my-project.es";
const { changeShow } = MyProject.useShow();
GitHub PR

Vue 3 Vite - dynamic image src

I'm using Vue 3 with Vite. And I have a problem with dynamic img src after Vite build for production. For static img src there's no problem.
<img src="/src/assets/images/my-image.png" alt="Image" class="logo"/>
It works well in both cases: when running in dev mode and after vite build as well. But I have some image names stored in database loaded dynamically (Menu icons). In that case I have to compose the path like this:
<img :src="'/src/assets/images/' + menuItem.iconSource" />
(menuItem.iconSource contains the name of the image like "my-image.png").
In this case it works when running the app in development mode, but not after production build. When Vite builds the app for the production the paths are changed (all assests are put into _assets folder). Static image sources are processed by Vite build and the paths are changed accordingly but it's not the case for the composed image sources. It simply takes /src/assets/images/ as a constant and doesn't change it (I can see it in network monitor when app throws 404 not found for image /src/assets/images/my-image.png).
I tried to find the solution, someone suggests using require() but I'm not sure vite can make use of it.
Update 2022: Vite 3.0.9 + Vue 3.2.38
Solutions for dynamic src binding:
1. With static URL
<script setup>
import imageUrl from '#/assets/images/logo.svg' // => or relative path
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2. With dynamic URL & relative path
<script setup>
const imageUrl = new URL(`./dir/${name}.png`, import.meta.url).href
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
3.With dynamic URL & absolute path
Due to Rollup Limitations, all imports must start relative to the importing file and should not start with a variable.
You have to replace the alias #/ with /src
<script setup>
const imageUrl = new URL('/src/assets/images/logo.svg', import.meta.url)
</script>
<template>
<img :src="imageUrl" alt="img" />
</template>
2022 answer: Vite 2.8.6 + Vue 3.2.31
Here is what worked for me for local and production build:
<script setup>
const imageUrl = new URL('./logo.png', import.meta.url).href
</script>
<template>
<img :src="imageUrl" />
</template>
Note that it doesn't work with SSR
Vite docs: new URL
Following the Vite documentation you can use the solution mentioned and explained here:
vite documentation
const imgUrl = new URL('./img.png', import.meta.url)
document.getElementById('hero-img').src = imgUrl
I'm using it in a computed property setting the paths dynamically like:
var imagePath = computed(() => {
switch (condition.value) {
case 1:
const imgUrl = new URL('../assets/1.jpg',
import.meta.url)
return imgUrl
break;
case 2:
const imgUrl2 = new URL('../assets/2.jpg',
import.meta.url)
return imgUrl2
break;
case 3:
const imgUrl3 = new URL('../assets/3.jpg',
import.meta.url)
return imgUrl3
break;
}
});
Works perfectly for me.
The simplest solution I've found for this is to put your images in the public folder located in your directory's root.
You can, for example, create an images folder inside the public folder, and then bind your images dynamically like this:
<template>
<img src:="`/images/${ dynamicImageName }.jpeg`"/>
</template>
Now your images should load correctly in both development and production.
Please try the following methods
const getSrc = (name) => {
const path = `/static/icon/${name}.svg`;
const modules = import.meta.globEager("/static/icon/*.svg");
return modules[path].default;
};
In the context of vite#2.x, you can use new URL(url, import.meta.url) to construct dynamic paths. This pattern also supports dynamic URLs via template literals.
For example:
<img :src="`/src/assets/images/${menuItem.iconSource}`" />
However you need to make sure your build.target support import.meta.url. According to Vite documentation, import.meta is a es2020 feature but vite#2.x use es2019 as default target. You need to set esbuild target in your vite.config.js:
// vite.config.js
export default defineConfig({
// ...other configs
optimizeDeps: {
esbuildOptions: {
target: 'es2020'
}
},
build: {
target: 'es2020'
}
})
All you need is to just create a function which allows you to generate a url.
from vite documentation static asset handling
const getImgUrl = (imageNameWithExtension)=> new URL(`./assets/${imageNameWithExtension}`, import.meta.url).href;
//use
<img :src="getImgUrl(image)" alt="...">
Use Vite's API import.meta.glob works well, I refer to steps from docs of webpack-to-vite. It lists some conversion items and error repair methods. It can even convert an old project to a vite project with one click. It’s great, I recommend it!
create a Model to save the imported modules, use async methods to dynamically import the modules and update them to the Model
// src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
const assets = import.meta.glob('../assets/**')
Vue.use(Vuex)
export default new Vuex.Store({
state: {
assets: {}
},
mutations: {
setAssets(state, data) {
state.assets = Object.assign({}, state.assets, data)
}
},
actions: {
async getAssets({ commit }, url) {
const getAsset = assets[url]
if (!getAsset) {
commit('setAssets', { [url]: ''})
} else {
const asset = await getAsset()
commit('setAssets', { [url]: asset.default })
}
}
}
})
use in .vue SFC
// img1.vue
<template>
<img :src="$store.state.assets['../assets/images/' + options.src]" />
</template>
<script>
export default {
name: "img1",
props: {
options: Object
},
watch: {
'options.src': {
handler (val) {
this.$store.dispatch('getAssets', `../assets/images/${val}`)
},
immediate: true,
deep: true
}
}
}
</script>
My enviroment:
vite v2.9.13
vue3 v3.2.37
In vite.config.js, assign #assets to src/assets
'#assets': resolve(__dirname, 'src/assets')
Example codes:
<template>
<div class="hstack gap-3 mx-auto">
<div class="form-check border" v-for="p in options" :key="p">
<div class="vstack gap-1">
<input class="form-check-input" type="radio" name="example" v-model="selected">
<img :src="imgUrl(p)" width="53" height="53" alt="">
</div>
</div>
</div>
</template>
<script>
import s1_0 from "#assets/pic1_sel.png";
import s1_1 from "#assets/pic1_normal.png";
import s2_0 from "#assets/pic2_sel.png";
import s2_1 from "#assets/pic2_normal.png";
import s3_0 from "#assets/pic3_sel.png";
import s3_1 from "#assets/pic3_normal.png";
export default {
props: {
'options': {
type: Object,
default: [1, 2, 3, 4]
}
},
data() {
return {
selected: null
}
},
methods: {
isSelected(val) {
return val === this.selected;
},
imgUrl(val) {
let isSel = this.isSelected(val);
switch(val) {
case 1:
case 2:
return (isSel ? s1_0 : s1_1);
case 3:
case 4:
return (isSel ? s2_0 : s2_1);
default:
return (isSel ? s3_0 : s3_1);
}
}
}
}
</script>
References:
Static Asset Handling of Vue3
Memo:
About require solution.
"Cannot find require variable" error from browser. So the answer with require not working for me.
It seems nodejs >= 14 no longer has require by default. See this thread. I tried the method, but my Vue3 + vite give me errors.
In Nuxt3 I made a composable that is able to be called upon to import dynamic images across my app. I expect you can use this code within a Vue component and get the desired effect.
const pngFiles = import.meta.glob('~/assets/**/*.png', {
//#ts-ignore
eager: true,
import: 'default',
})
export const usePNG = (path: string): string => {
// #ts-expect-error: wrong type info
return pngFiles['/assets/' + path + '.png']
}
sources
If you have a limited number of images to use, you could import all of them like this into your component. You could then switch them based on a prop to the component.