A custom loader for importing Vue single-file components
Testing Vue single-file components can be hard to setup. Basically you need to use a build tool to be able to test them which can be difficult to properly configure. If you're using Vite, you can use Vitest for testing, but sometimes it's just easier to use good ol' mocha.
In Node it is possible to write custom loader hooks which allow you to natively import .vue
files and transpile them on the fly, avoiding the need for a build tool to test your Vue single-file components on Node.
This is exactly what vue-esm-loader
does.
In that way, you can compare it to @vitejs/plugin-vue for Vite, or vue-loader for webpack, but then to natively import .vue
files in Node.
npm install --save-dev vue-esm-loader
If you're only importing .vue
files, you can use it as a standalone loader as
node --import vue-esm-loader/register ./your/file.js
so that you can do
// # ./your/file.js
import Component from './component.vue';
However, most of the time you will probably be using vue-esm-loader
in composition with other loaders, which can be done by manually registering vue-esm-loader
:
// # setup-loader.js
import { register } from 'node:module';
import * as vue from 'vue-esm-loader';
vue.register();
register('another-loader', import.meta.url);
and then
node --import ./setup-loader.js ./your-file.js
Using the register()
function manually also allows you to customize the loader's behavior.
For example, if for some reason you're also using .vuex
as an extension, you can set it up as
// # setup-loader.js
import * as vue from 'vue-esm-loader';
vue.register({
include: [/\.vuex?$/],
exclude: [/\.ce\.vue$/],
transformAssetUrls: {},
compilerOptions: {
whitespace: 'preserve',
},
});
If you're using vue-esm-loader
with mocha to test your .vue
files, you can create a .mocharc.cjs
file
// .mocharc.cjs
module.exports = {
// As standalone loader with default options
'import': 'vue-esm-loader/register',
// In composition with other loaders
'import': './setup-loader.js',
};
Using TypeScript in your .vue
files is supported out of the box.
For example
<template>
<p>Hello {{ name }}!</p>
</template>
<script lang="ts" setup>
const name : string = 'Vue';
</script>
will just work, even if you don't have setup a loader for .ts
files.
vue-esm-loader
uses esbuild under the hood to strip the types and output pure JS.
Note that it does not do type checking for you!
Note that importing .ts
files is not supported out of the box.
In order for this to work, you need to compose it with a loader that is able to handle .ts
files - see the example below.
If you are importing .ts
files from within your .vue
files, you need to setup a separate loader that can handle .ts
files.
For example,
<template>
<p>Hello {{ name }}!</p>
</template>
<script lang="ts" setup>
import { name } from './person.ts';
</script>
// # person.ts
export const name : string = 'TypeScript';
can be loaded with
// # setup-loader.js
import * as vue from 'vue-esm-loader';
import * as tsx from 'tsx/esm/api';
// IMPORTANT! tsx needs to be registered **after** vue-esm-loader, otherwise
// tsx will try to transform .vue files!
vue.register();
tsx.register();
If you want to compile markdown to .vue
files like Vitepress does, for example
# Hello
This is some **Markdown**.
then you can use the following loader configuration
// # setup-loader.js
import { register } from 'node:module';
import * as vue from 'vue-esm-loader';
// Register your markdown loader **before** vue-esm-loader so that it will
// transform markdown to vue first, and then have it processed by vue-esm-loader.
register('./markdown-loader.js', import.meta.url);
vue.register({
// Both .vue and .md files have to be processed by vue-esm-loader.
// You can use either a regex or a glob.
include: [/\.vue$/, '**/*.md'],
});
// # markdown-loader.js
import marked from 'marked';
export async function load(req, ctx, nextLoad) {
let url = new URL(req);
if (url.pathname.endsWith('.md')) {
let { source } = await nextLoad(req, { format: 'module' });
return {
source: `<template>${marked(String(source)}</template>`,
format: 'module',
};
}
// Use the default loader for files with no .md extension.
return nextLoad(req, ctx);
}
Most html preprocessors are supported out of the box, though you need to make sure to have them in your node_modules
folder.
For example, if you'd like to write your templates with pug, you can do so
<template lang="pug">
div
p We're using {{ template }}
</template>
<script setup>
const template = 'Pug';
</script>
provided that you have installed pug with npm install pug
.
Versions 0.x
initially relied on create-esm-loader to make vue-esm-loader
composable with other loaders.
Since the advent of the register()
api of node:module
, this is now no longer necessary.
This also means that the minimum supported Node.js version is ^18.19.0 || >= 20.6.0
as module.register()
is not available in earlier versions.
If you are still using create-esm-loader for other loaders, they can normally be combined just fine provided that you register the create-esm-loader
-created loader last:
// # setup-loader.js
import * as vue from 'vue-esm-loader';
vue.register();
register('./cesm-loader.js', import.meta.url);
// # cesm-loader.js
import create from 'create-esm-loader';
export const {
resolve,
load,
} = await create(config);
Note that with module.register()
loaders are now easily composable, so there is actually no need anymore for create-esm-loader, and hence it is advised to migrate away from it.
This might not be possible straight away if you rely on loaders that use create-esm-loader under the hood, but you are invited to make the maintainers of these loaders aware of this.
If you're using node-esm-loader, then it is advised to import multiple files
node --import ./setup-loader.js --import node-esm-loader/register ./your-file.js
until you no longer need any loaders that rely on create-esm-loader under the hood.
vue-esm-loader
is heavily inspired by the official @vitejs/plugin-vue for Vite.
If you're familiar with how this plugin works works, you'll find vue-esm-loader
to be really familiar.
Basically it transforms the .vue
file in the load()
hook into something that looks to Node like
import { render, staticRenderFns } from './file.vue?type=template';
import script from './file.vue?type=script';
export default {
...script,
render,
staticRenderFns,
};
and then subsequently the querystring is sniffed to return the correct code blocks, while also compiling the template using @vue/compiler-sfc
in the load()
hook.