highlightjs / highlight.js

JavaScript syntax highlighter with language auto-detection and zero dependencies.
https://highlightjs.org/
BSD 3-Clause "New" or "Revised" License
23.65k stars 3.59k forks source link

Vue.js plugin "ReferenceError: hljs is not defined" #2815

Closed esoterra closed 3 years ago

esoterra commented 3 years ago

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.
joshgoebel commented 3 years ago

A Vue.js plugin should be self-contained and not require an external script.

Are there specific docs to this effect or are you just stating this as a generally good principle?

The terminology here is also a little muddled (I think) since we aren't requiring an "external script"... the plugin ships as a tiny part of the larger HLJS package - and indeed makes no sense to use without it. The fact that you can even access x.vuePlugin is a guarantee the library has already been loaded.

I can leave this open a bit but I think I'd first like to resolve the other issue - whether our browser build and npm build indeed should have the same behavior when used inside a browser. This inconsistency has caused problems in the past and perhaps it's time to finally resolve it. That of course would also solve the actual error here - making this purely an architectural choice (rather than a fix).

So lets see what happens with that first.

esoterra commented 3 years ago

I'll find time later today to answer your questions on both this and the PR more thoroughly.

Vue.js templates are not allowed to include script tags, so if a user creates a component that uses this component they are unable to include the script "within" their components definition so that the script is in scope.

Setup for using a Vue component usually involves

  1. installing the lib
  2. Adding Vue.use(...) to main.js
  3. Importing the component and adding it to the components property

It's highly unusual for any other dependency or setup to be necessary to make the component itself work.

joshgoebel commented 3 years ago

Looking forward to your more thorough response.

It's highly unusual for any other dependency or setup to be necessary to make the component itself work.

If my PR is merged (or you were using the browser build) this statement would already be true... no "extra" setup is necessary [other than first of course loading HLJS to get access to the plug-in in the first place], unless you're saying the bundling itself is part of the problem (which I didn't see addressed in your PR). The browser build already just works because a core assumption [of our browser build] is that hljs is a browser global.

But also I'm not sure what this statement is trying to say... if our component requires Highlight.js to function then surely external setup is necessary whether that setup is done globally or whether it's done in a smaller scope and then injected into the plugin. IE, you have to get the dependency one way or another?

esoterra commented 3 years ago

It's that loading part that I'm suggesting "shouldn't" be necessary. I'm slightly confused by your second paragraph, with my suggested change the three steps I listed above would be entirely sufficient. In performing 2, the user imports from highlight.js and causes the VuePlugin to be correctly bound to hljs. (as clarification "other dependency or setup" is to be read as "other dependency or setup than these 3 above")

weaversam8 commented 3 years ago

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.

joshgoebel commented 3 years ago

It's that loading part that I'm suggesting "shouldn't" be necessary.

I don't follow... it seems you must load or require the library:

const highlightjs = import("highlightjs/core")

or:

<script src="highlight.js"> ...

That's all I mean when I say "loading" the library... without this "loading" you can't access hljs.vuePlugin in order to pass it to use.

weaversam8 commented 3 years ago

@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.

joshgoebel commented 3 years ago

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.

The way you speak about Vue components I'm wondering if our exposing it via an interface on our main instance even fits the overall culture then... because it seems we're doing the opposite of what you state here. Perhaps it would better be exposed as a separate thing entirely, or as it's own npm library?

esoterra commented 3 years ago

While that certainly could be done, I don't know why when this is so close to working really well for Vue.js and highlight.js that it'd be removed/extracted/rebuilt instead of patched.

weaversam8 commented 3 years ago

For reference, metachris/vue-highlightjs exposes a Vue Highlight.js component that follows these patterns. You could either scrap your implementation (and point to theirs) or integrate their work here.

Their repository also provides an example of a completely headless test, demonstrating how it's possible to use the library without any implicit dependencies.

joshgoebel commented 3 years ago

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. :)

weaversam8 commented 3 years ago

Yeah, staying on top of every little project is an impossible task :joy:

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

I'm not sure if this is true. Your PR seems to automatically add hljs to the window object, which would work in some circumstances, but in other environments (like Vue SSR) there is no window object. Not sure your PR would've resolved this issue in this environment.

joshgoebel commented 3 years ago

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.

In your example, maybe. But as it stands now it can't be pruned (afaik) because there is only a single object/instance. If you pruned all that code then there is nothing to export... the code has to run to generate the exported value. Correct me if I'm wrong.

export default HLJS({});

So in order to get vuePlugin that code has to run, and hence it's possible for it to create a global. I'm not arguing this is best, I merely opened the discussion as this has come up before (the inconsistency between behavior of npm vs browser builds when used in the browser). Some people are confused by this.

joshgoebel commented 3 years ago

but in other environments (like Vue SSR) there is no window object

Yeah, I think that's the strongest absolute reason I've heard so far that this (or some other small change) may need to happen here. :-) So I'll take another look at this with Vue SSR in mind. :)

weaversam8 commented 3 years ago

I see what you mean - yeah, if there was a window object #2818 would work, even if that does subvert the normal process for dependency resolution in some cases. Might be worth merging that one just to cover some of those edge cases unrelated to Vue.

joshgoebel commented 3 years ago

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. :-)

mhio commented 3 years ago

I just ran into this adding highlight.js to a vue web app. I moved the existing vue plugin and component into the HLJS scope where everything else that uses hljs is. Seems to work.

I would only expect hljs to be available globally if I was adding the js dist via in a html file. Since react/vue came about, everything I've been involved with has been yarn/npm and webpack based.

edit - I should refresh before commenting :)

joshgoebel commented 3 years ago

I moved the existing vue plugin and component into the HLJS scope where everything else that uses hljs is. Seems to work.

That would also solve it...

joshgoebel commented 3 years ago

So for v11 we're going to be pulling the plugin outside the the main library and shipping it as a separate file... there was really no reason this should have been always compiled in since not everyone uses Vue - so it's dead weight for many. Here is what I have right now:

<script src="../build/highlight.js"></script>
<script src="../build/vue_plugin.js"></script>

let plugin = hljsVue.BuildVuePlugin(hljs).VuePlugin;
Vue.use(plugin);

If using a bundler:

import { BuildVuePlugin } from "../build/vue_plugin.js";

Does that look about like what you'd expect?

weaversam8 commented 3 years ago

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));
joshgoebel commented 3 years ago

since that wouldn't allow the developer to selectively import different languages / control import bloat.

Right. The two things kind of have to happen separately.

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

There is really no reason most people should be pulling in all languages, and that is doubly true for usage in a web browser. So this is usually not what people want nor something we'd wish to encourage.

Vue.use(buildVuePlugin(hljs));

Right now I purposely export both the component AND the plugin... isn't that maximally flexible incase someone wanted to rename the tag?

weaversam8 commented 3 years ago

There is really no reason most people should be pulling in all languages, and that is doubly true for usage in a web browser. So this is usually not what people want nor something we'd wish to encourage.

I agree from a performance standpoint. I think from a DX perspective it still might be useful to be able to just import a completely working plugin (to make it as simple and easy as possible.) Maybe it's possible to pick like... the 5 most common languages and make those the default import?

You're certainly more knowledgeable about the languages your users use though - I defer to you on the ultimate decision on whether something like that would be more useful.

Right now I purposely export both the component AND the plugin... isn't that maximally flexible incase someone wanted to rename the tag?

The need to do something like Vue.use(buildVuePlugin(hljs).VuePlugin) is still... complicated. I think the need to rename the tag is rare. The way I might do it would be to make a separate export like buildVueComponent(hljs) that would allow users to do something like Vue.component('custom-hljs-component-name', buildVueComponent(hljs)); and still do Vue.use(buildVuePlugin(hljs));

(buildVuePlugin(hljs) could call buildVueComponent(...) under the hood to reduce duplication... I just think that it should return the plugin itself rather than an object containing the plugin and the custom component.)

Edit: An alternative approach could be to expose the component as a property on the Vue plugin exported by buildVuePlugin itself. The export could look something like this:

{
    install: function(Vue, options) { ... },
    component: HLJSVueComponent
}

This would enable users to Vue.use(buildVuePlugin(hljs)); directly or enable users to register the component with a custom name by doing Vue.component('custom-hljs-component-name', buildVuePlugin(hljs).component);

joshgoebel commented 3 years ago

An alternative approach could be to expose the component as a property on the Vue plugin exported by buildVuePlugin itself.

Now that sounds like a great idea.

Maybe it's possible to pick like... the 5 most common languages and make those the default import?

I don't think any list would ever be correct, and that's forgetting about the fact that we allow plugins and have configuration knobs as well... Highlight.js is a complex library in its own right... it's not really suited to be silently hidden inside an auxiliary component. Perhaps the require cache and the way import functions already solves this for us though?... perhaps we could just require highlightjs/core... then we'd eliminate the passing of hljs - yet someone would still need to require/import the languages somewhere else or their highlighter isn't going to be doing anything useful.

Would that be better? I'm not sure.

weaversam8 commented 3 years ago

perhaps we could just require highlightjs/core... then we'd eliminate the passing of hljs - yet someone would still need to require/import the languages somewhere else or their highlighter isn't going to be doing anything useful.

Yeah, that might be nice! Could look something like this:

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

HLJSVuePlugin.registerLanguage('javascript', javascript); // alias for HLJSVuePlugin.hljs.registerLanguage(...)

Vue.use(HLJSVuePlugin);
joshgoebel commented 3 years ago

No, that would require proxying everything thru the plugin, which just adds complexity and room for error. I'm talking about:

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

hljs.registerLanguage('javascript', javascript);

Vue.use(vuePlugin);

The only difference being you don't have to pass anything to the plugin, you still have to import hljs yourself (apart from it's use in the plugin) so that you can configure it properly, etc...

weaversam8 commented 3 years ago

That doesn't work though - it either (1) relies on the Vue plugin being able to access hljs through the global scope (the original issue I mentioned in this comment) or (2) depends on nonobvious behavior (the Vue plugin using a different HLJS instance but the registerLanguage method calls are somehow shared between them?)

Why does exposing the hljs instance through the Vue plugin introduce room for error? You're just exposing the underlying instance instead of importing it - you can manipulate it in all the normal ways.

joshgoebel commented 3 years ago

(1) relies on the Vue plugin being able to access hljs through the global scope

Not trying to go back to this...

(2) depends on nonobvious behavior (the Vue plugin using a different HLJS instance but the registerLanguage method calls are somehow shared between them?)

It wouldn't be a separate instance. It's exactly the same instance. Works with require (I just tested it) though I suppose perhaps a bundler could somehow break this behavior/guarantee. But that should be an intrinsic properly of require/import - that importing the same thing gets you the same object/instance.

Why does exposing the hljs instance through the Vue plugin introduce room for error?

If you just exported hljs itself as a property of the plugin that'd be one thing... but to me that is so very, very backwards. Proxying the calls though adds complexity - additional complexity always introduces room for error. There is no guarantee that someone using Highlight.js with a Vue plugin might not also want/need to use it outside that same plugin - so now it's getting imported twice in any case... and making the library the plugin the "source" of the library globally (if you're using it elsewhere) seems very backwards vs a direct dependency.

Also a proxy would seem to hint/imply that the plugin has it's own internal working copy of Highlight.js, but that is not our architecture... there is a single singleton HLJS object... so configuring the plugin would be configuring HLJS everywhere...

weaversam8 commented 3 years ago

It wouldn't be a separate instance. It's exactly the same instance. Works with require (I just tested it) though I suppose perhaps a bundler could somehow break this behavior/guarantee. But that should be an intrinsic properly of require/import - that importing the same thing gets you the same object/instance.

I assumed that this wasn't an intrinsic property of require / import but I think you're actually correct. I'm still trying to find proof that most bundlers shouldn't break this but I think this is a misconception I have from older module systems (CommonJS + others).

Also a proxy would seem to hint/imply that the plugin has it's own internal working copy of Highlight.js, but that is not our architecture... there is a single singleton HLJS object... so configuring the plugin would be configuring HLJS everywhere...

This makes sense. I assume this singleton is window.hljs or something for scripts in a browser context, but how is this singleton enforced in a Node / ES6 context? Do you mean that this singleton is enforced by the module loading behavior mentioned above (a single shared instance between all imports?)

joshgoebel commented 3 years ago

I assume this singleton is window.hljs or something for scripts in a browser context

Yes, there is actually no way to create a second HLJS instance, it's never been necessary.

Do you mean that this singleton is enforced by the module loading behavior mentioned above (a single shared instance between all imports?)

Yes.

weaversam8 commented 3 years ago

Intuitively, to me, this example still seems confusing. I'm not sure how many people have the mental model that different imports of a library are the same instance. Even if they do, it's not obvious that hljs-vue-plugin imports highlight.js/lib/core... people aren't going to know that. This example seems a bit too "magical," even if it does work.

If you just exported hljs itself as a property of the plugin that'd be one thing... but to me that is so very, very backwards. Proxying the calls though adds complexity - additional complexity always introduces room for error.

I mean, sure, I guess? I'm not sure I see a specific example where this could cause an error though. Do you have one in mind?

I think that HLJSVuePlugin.registerLanguage is a nice syntactic sugar for HLJSVuePlugin.hljs.registerLanguage, but that's not the hill I'm going to die on. If you really think that's dangerous for some reason the second option seems fine to me too, and preferable to the strange nonobvious link.

There is no guarantee that someone using Highlight.js with a Vue plugin might not also want/need to use it outside that same plugin - so now it's getting imported twice in any case... and making the library the plugin the "source" of the library globally (if you're using it elsewhere) seems very backwards vs a direct dependency.

I think if someone has a need to use it outside the plugin, then they have a reason to import. If they don't, then forcing them to import it anyways and then making the link nonobvious just seems like it creates more room for head-scratching.

joshgoebel commented 3 years ago

I'm not sure how many people have the mental model that different imports of a library are the same instance.

If that's reality then perhaps it's time they learned. :-) I'd also suggest that's the job of documentation. https://github.com/highlightjs/vue-plugin#using-es6-modules--bundling

Even if they do, it's not obvious that hljs-vue-plugin imports highlight.js/lib/core... people aren't going to know that.

You don't have to know EXACTLY what it imports, only that it's uses Highlight.js... to use Highlight.js you have to import "core" at some point, and that import is then a singleton - shared everywhere. Hence you can also import a common subset (even even index), and that should also just work:

import hljs from 'highlight.js/lib/common';
import vuePlugin from "@highlightjs/vue-plugin";
Vue.use(vuePlugin());

I'm not sure I see a specific example where this could cause an error though.

I was only talking about the general principle of added complexity. And of course this requires additional maintenance anytime we change the API, etc... it's not "free".

I think that HLJSVuePlugin.registerLanguage is a nice syntactic sugar for HLJSVuePlugin.hljs.registerLanguage

Sure, but then you may need unregisterLanguage, configure, registerAliases, addPlugin, etc... might as well just use the APIs we already have... 😄

weaversam8 commented 3 years ago

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!

joshgoebel commented 3 years ago

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.

This is legacy (long before me), but also since it works and isn't broken... I've had no reason to re-consider the behavior.

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.

weaversam8 commented 3 years ago

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.

joshgoebel commented 3 years ago

Done. https://github.com/highlightjs/vue-plugin/blob/main/README.md

mhio commented 3 years ago

Any import of Highlight.js refers to the same singleton instance of the library, so configuring the library anywhere configures it everywhere.

The module singleton works most of the time but it's not guaranteed across submodules in node.js.

A module "singleton" is based on the resolved file path of the imported module url (that's not a "great article" but it does define the import process).

Normally, when version requirements match, all packages will collapse down to a single highlight.js in the parent package. It is perfectly acceptable for the main project code and every sub module to rely on a separate version of highlight.js though as they can each resolve to their own node_modules copy of the module and each have their own "singleton" (per resolved file path).

I haven't dug into the new vue plugin completely so if there is some window.hljs magic happening that was briefly mentioned above ignore this...

It looks like with the current setup @highlightjs/vue-plugin has a dependency of "highlight.js": "^10.7.1". If the parent package that imports @highlightjs/vue-plugin had a lock file at 10.7.0 the project would end up structured like this:

my-super-project/
  node_modules/
    highlight.js/ (10.7.0 "singleton")
    @hightlightjs/
      vue-plugin/
        node_modules/
          highlight.js/ (10.7.1 "singleton")

Here the docco example hljs.registerLanguage('javascript', javascript) would not be seen by the hljs instance in the @highlightjs/vue-plugin module.

Barring the parent package injecting hljs into the plugin already suggested above, you start needing peer dependencies which have their own issues.

It's a bit of a wat moment when you do run into the "not quite a singleton" issue in the wild, so personally I only rely on module singletons within a single module/package. The super esoteric version of this I've seen is when running on case insensitive file systems and one file in your project does an import on a differently cased name of a file : /

mhio commented 3 years ago

A quick demo of mismatched versions. I needed to add an export of hljs into node_modules/@highlightjs/vue-plugin/src/vue.js to demonstrate.

joshgoebel commented 3 years ago

It is perfectly acceptable for the main project code and every sub module to rely on a separate version of highlight.js though as they can each resolve to their own node_modules copy of the module and each have their own "singleton" (per resolved file path).

At first blush that sounds like a terrible idea leading to all sorts of hard to diagnose issues.

It looks like with the current setup @highlightjs/vue-plugin has a dependency of "highlight.js": "^10.7.1". If the parent package that imports @highlightjs/vue-plugin had a lock file at 10.7.0

Can we fix that by vue-plugin just declaring the dependency more loosely? There is no reason it needs to be quite so tight. Do you have a solution you'd recommend?

It's a bit of a wat moment when you do run into the "not quite a singleton" issue in the wild

I'm also a bit curious how often this happen in practice vs a hypothetical.

mhio commented 3 years ago

Can we fix that by vue-plugin just declaring the dependency more loosely? There is no reason it needs to be quite so tight. Do you have a solution you'd recommend?

Setting the wide dependency version range would cover any minor version issues up to a major version bump of highlight.js (e.g. yarn upgrade highlight.js@11 yields no change as the Vue plugin continues happily along on 10, at least a major mismatch has more chance of being picked up straight away).

Not defining it as a dependency is probably more of a fix, either by using injecting the hljs instance as before or setting highlight.js as a peerDependency so it won't be installed by @highlightjs/vue-plugin. Devs would at least get an install time warning with a peerDependency rather than a silent mismatch. Injection maybe provides an added entry point where some checking could happen if needed.

I don't think relying on @highlightjs/vue-plugin completely easily fits with the language import system in highlightjs but I think this was covered above:

import { hljs, vuePlugin } from '@highlightjs/vue-plugin'

I'm also a bit curious how often this happen in practice vs a hypothetical.

I may be an outlier as I've used the module singleton pattern (or tried to use it :) a fair bit. I've inflicted this on my self on occasion. As mentioned I've limited their use to only rely on a singleton inside a single package, or maybe across packages in a mono repo if I'm feeling adventurous. I'll try dig out the gh issues where I ran into it in the wild.

People who use ^ semver matchers everywhere will be mostly fine until something get's a major bump. The more ~ and straight = semver matchers the more likely to run into the more subtle versions of the issue.

The issue is certainly less prevalent now that yarn and npm have stabilised somewhat. There was a period where depending on which packager or major version of npm was in use you could get vastly different module layouts.

It's not common, just a horrible issue to diagnose when 99% of your app is ok but 1% is "not picking up that damn setting! bangs keyboard". I certainly didn't read through those node cjs/esm load specs for fun.