highlight.js: Vue.js plugin "ReferenceError: hljs is not defined"

A Vue.js plugin should be self-contained and not require an external script. Currently however, the Vue.js component for highlight.js depends on hljs but hljs is not in scope in its definition. https://github.com/highlightjs/highlight.js/blob/master/src/plugins/vue.js#L24

If a Vue.js user uses npm or yarn to install highlight.js, and follows the instructions in the section Using with Vue.js they will receive the following error in their browser. [Vue warn]: Error in render: "ReferenceError: hljs is not defined" The line that produces this exception is this one: https://github.com/highlightjs/highlight.js/blob/master/src/highlight.js#L26

Proposal:

  1. Put the contents of the vue.js plugin file inside a function that accepts hljs as an argument and returns VuePlugin. Make the file export that function.
  2. Change the plugins/vue.js import to import this new function instead of VuePlugin
  3. Change this location to invoke the wrapper function.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 2
  • Comments: 39 (22 by maintainers)

Commits related to this issue

Most upvoted comments

If you’re offering a plugin for Vue that provides a component, as a Vue developer I’d generally expect that component to be completely self-contained, where it imports the dependencies it needs. This makes it compatible with any module bundler which will automatically resolve and bundle those dependencies.

Depending on a global being in scope is uncharacteristic of what I think of as a modern Vue component, since Vue is used in many different contexts other than simply being included on a page. Vue is now used in projects (like the Gridsome static site generator, which does do Vue SSR) that manage dependencies automatically (users aren’t usually writing their own <head> tags anymore, in many cases they’re being automatically managed by a build tool.)

Updating the component to automatically import HLJS would allow it to be used anywhere where Vue components can be used, without any issues. If that option isn’t ideal (if it, for example, creates duplicate imports of HLJS) then dependency injection (like what’s implemented in #2816) is the next best option.

Might be worth merging that one just to cover some of those edge cases unrelated to Vue.

Well, I see arguments both ways. I’m trying to remember if the past issue or two were all Vue related or if there was some larger confusion. Globals are indeed dirty, but that ship has sailed on the core web build with hljs… and I’ve had this thought before (about people using npm in the browser), so seemed worth considering. I’ll count both of you as “no globals” votes I think. 😃

@joshgoebel there are import systems that don’t place all imports in global scope. If HLJS requires a variable hljs to be defined in global scope, it’s incompatible with these import systems, and by extension certain environments.

For example, in some environments you can do something like this:

import { core: hljs, vuePlugin } from "highlightjs";

Vue.use(vuePlugin);

The import of hljs here will be pruned automatically by most import systems, since it isn’t being used anywhere. Modern import systems like this don’t rely on global scope at all.

I might explain the singleton pattern … more explicitly rather than calling it “the magic of ES6 modules”

If you know a great article or something on the subject I’d be happy to link to one. I know WHAT happens but I’m not sure I have a better explanation for it personally. I spent a few minutes looking for didn’t find anything great IMHO.

I meant literally mentioning that Highlight.js uses a singleton instance. Maybe something like:

Thanks to the magic of ES6 modules you can import Highlight.js anywhere you need in order to register languages or configure the library. Any import of Highlight.js refers to the same singleton instance of the library, so configuring the library anywhere will configure it everywhere.

Added text emphasized.

I think something else here that I failed to put my finger on earlier is that many (most?) ES6 module imports import a class that’s instantiated using new rather than a singleton. Maybe it’s just a “me” thing but I’m not used to the singleton pattern here.

The docs explaining this probably clears up any confusion. I might explain the singleton pattern (and how ES6 modules actually implement and enforce that pattern) more explicitly rather than calling it “the magic of ES6 modules” but this otherwise looks great!

In this comment I wrote about the scope and pruning issues with the current approach.

The import of hljs here will be pruned automatically by most import systems, since it isn’t being used anywhere. Modern import systems like this don’t rely on global scope at all.

The code sample you just provided looks like it solves both of these problems! 🙂

Does that look about like what you’d expect?

The pattern of importing BuildVuePlugin and then passing it an hljs instance seems atypical. I understand what’s happening, but why can’t you just import a built Vue plugin directly? Also, since the Vue plugin will always require HLJS, why not declare it as a direct dependency instead of requiring it as an implicit peer dependency?

Something like this would be what I’d expect this to look like:

import { HLJSVuePlugin } from "hljs-vue-plugin";

Vue.use(HLJSVuePlugin);

Edit: I realize why you might not want to automatically import HLJS in the Vue component, since that wouldn’t allow the developer to selectively import different languages / control import bloat.

I think an appropriate solution for this would be to export HLJSVuePlugin (or something like that) as the default export which would import and configure HLJS for all languages (the simplest developer experience) and then offer up the BuildVuePlugin approach from the same package if developers wanted more fine-grained control over the HLJS instance used in the Vue Plugin.

That approach might look like this:

import hljs from 'highlight.js/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';
import { buildVuePlugin } from "hljs-vue-plugin";

hljs.registerLanguage('javascript', javascript);

Vue.use(buildVuePlugin(hljs));

I’m just trying to understand the whole scope, because this has come up before… and if my PR had been merged 2 months ago (or you were using a browser build) then you never would have opened this issue because you’d never have run into the problem. So hence exploring whether one issues resolves the other or whether there are two different issues here. Measure twice, cut once and all. 😃

that it’d be removed/extracted/rebuilt instead of patched.

The only time to discuss many of these nuanced items is when they come up in a real world context… so it seems worth asking (even if we keep it in core) if perhaps it should be exported differently (whether the dependency should be inverted, etc), etc.

For reference, metachris/vue-highlightjs exposes a Vue Highlight.js component that follows these patterns.

We need to do a better job of tracking the overall Highlight.js ecosystem - I have far too little what is out there other than 3rd party language modules… If there was already a great Vue plugin I’m not sure I’d have added it to the core library at all. But that ship sailed. 😃