Repository navigation
Import Prism without creating the global instance - #4150
DmitrySharabin wants to merge 7 commits into
Conversation
✅ Deploy Preview for dev-prismjs-com ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
@LeaVerou, a question about the It came with d5d7a5a and acff03f, when these functions moved out of the As far as I can tell, nothing calls these functions without import { highlight } from "prismjs/core.js";
highlight("let a = 1", "javascript"); // uses the global instanceIf so, I'd keep them instance-agnostic some other way, without importing the global instance. If not, removing the fallback keeps them as plain |
Yes, but it's not a super high priority goal if it adds significant complexity. It's a nice to have. |
I'd leave it for the next PR then. |
|
Oof, that's …not an acceptable tradeoff. We can't have every language self-adding to the singleton like this. 😕 |
0d3b2e4 to
1fdc4c1
Compare
Agreed. I followed your advice and now languages and plugins add themselves to a registry that does not depend on the A language now imports only the registry. The static |
abc33bc to
a716195
Compare
|
We should not be adding any globals, even if they are symbols (except for the IIFE build, obviously) |
There was a problem hiding this comment.
🤖 Reviewed at a716195. CI is green and the core tests pass locally. I also verified by hand that importing src/core.js creates neither globalThis.Prism nor the registry global, while src/global.js does create the registry global.
Overall: the direction is right and the core cleanup is good. Removing the this ?? singleton fallbacks leaves the class with no dependency on config or the global instance, which is the main promise of the PR, and it holds. But the registry mechanism has design problems, there is one behavioral regression, and the PR cannot be exercised end to end on its own since nothing in src/ produces into the registry until #4156 and #4158 land.
Blocking
- The
Symbol.for('prismjs.registry')global is unnecessary, not just undesirable. Its only purpose is to let ESM-imported languages reach an IIFE-created instance. Wiring the module-local registry into whichever instancecore/prism.jsends up with (new or reused) achieves the same thing with no global. Details onsrc/registry.js. constructor.name === 'Prism'is not a reliable instance check, and this PR makes it the gate for the whole registry wiring. It depends onkeep_classnamessurviving every downstream minifier, and it accepts any unrelatedwindow.Prismwith a same-named constructor. Needs an explicit brand or at least a duck-type check, used consistently bycore/prism.jsandconfig.js. Details onsrc/core/prism.js.- Double
highlightAll()when the IIFE is loaded andprismjsis imported afterwards. The reused instance'sreadyhas already settled, so auto-start fires immediately. Details onsrc/auto-start.js.
Should fix before merge
- The new test only exercises the
addevent path, not the replay loop that handles the common "language imported before the instance exists" order, and it leaks into the process-wide registry. manualis half removed: gone fromPrismConfig, still present (untyped) on the global instance'sconfig, andnew Prism({ manual: true })now silently does nothing different.- Two new public entry points, a new public method and a removed option, with no docs changes.
Minor
load-languages.jsandautoloader.jsstill calllanguageRegistry.adddirectly instead ofregister().ComponentRegistry.load()with nopathimports from the literal string"undefined…".
What is good
- Dropping the singleton import from
highlight,tokenize,_matchGrammarandresolveis clean, and the existing@this {Prism}annotations keep TypeScript checking them. - Moving the
./languages/and./plugins/defaults into the constructor is the right place now thatnew Prism()no longer reads page config. - The
Pluginconstructor change toregistry.prism.register(def)is behavior-preserving and removes duplicated dispatch logic.
Generated by Claude Code
Done in f7609c4. Changes since the review:
The classic build and the module build now have separate registries, so each setup loads components its own way. The descriptions of #4150, #4156 and #4158 show both. Ready for another round. |
731a6aa to
02de9b7
Compare
|
🤖 Second round, on 02de9b7. CI is green, and I ran the core and plugin suites, the TypeScript check and a full build locally, all clean. Every item from the first review is addressed, and well: the module-local registry with @LeaVerou and I talked this through; the conclusions below are the plan for the PR and two follow-ups. 1. The auto-start guard only covers IIFE-firstA Fix: one singleton regardless of load order. Separate module graphs share nothing but globals and the DOM, so for a later copy to find an earlier one, the earlier one has to publish. Use the name Prism already owns, not a new one:
2.
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The data-insert plugin now runs in `transform`: Rollup tree-shaking read the source `'dev'` version as a constant and dropped the major-version check from the IIFE build Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
02de9b7 to
410ad2c
Compare
|
Done in 46796a9, 410ad2c, 4cb0378 and cff4b6b. Changes since your second review:
The PR body now shows the module layout. One build change: the data-insert plugin now replaces placeholders in The bundler problems and the idempotent-highlighting follow-up are in separate issues: #4165, #4166, #4167. |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cff4b6b to
0b01e6d
Compare
Wait, what? We may not want a global instance at all. This should only happen if the IIFE build is included. I.e. it's the job of the IIFE build: Check if a compatible global instance already exists, and use that if so. It should also be used as the singleton for ESM versions. Basically, unless we have incompatible Prism versions, there is one singleton that everything points to, and whether that's also a global is determined by whether the IIFE build is included.
Doesn't this mean that including a language now has to pull in the singleton again?
Unbuilt source is not compatible with everything though… But that's not super important.
? I’m a little worried we're overengineering this and introducing a lot of complexity that is not warranted by the use cases. Can you please edit the PR with a list of actual, concrete problems this is solving? |
A module copy sets If that order is not worth a global, I'll drop it. Then only the classic build sets
No. A language imports
Agreed.
Nothing changes against
The description now lists the concrete cases. Each one comes from #4149 or from one of your reviews:
Your comment above removes case 5. Should I drop it? |
|
The non-IIFE build can absolutely not create a global, that's a hard requirement. Imagine Prism being used as a dependency of something else and leaking globals into the consumer environment! What I was suggesting is that the IIFE build should check if
What I was saying is that
It still lists abstract, theoretical cases. Have any of these come up in any real projects, ours or of our users, or are we complicating the codebase over some abstract worry? |
|
Closing this in favor of #4168, which starts over from
Everything added for load orders nobody has reported is gone: module code publishing the global, Thank you for the reviews, they shaped the much smaller version. |
In the spirit of simple things should be easy, complex things should be possible (#4149 (comment)), each entry now does less than the one above it. This PR changes only the core. Languages register through it in #4156, plugins in #4158.
Cases this PR fixes, each with what
v2does today:import { Prism } from "prismjs"andnew Prism({ manual: true }). Onv2, the import also creates the global instance, and that instance highlights the page (Should plugins register themselves, and how do we import thePrismclass without side effects? #4149). Nowprismjs/core.jsgives the class without creating an instance.v2, the plugin also joins the global instance (Should plugins register themselves, and how do we import thePrismclass without side effects? #4149). Now a plugin joins only the instances it is registered with (with Import plugins without creating the global instance #4158).prismjs. Onv2, the module copy creates a second instance, which highlights the page again. Now the module copy uses the instance of the classic build and does not highlight.v2, a plugin reaches the classic instance only if its class name survives minification. Now languages and plugins reach it through the shared registry (with Register languages with the global instance on import #4156 and Import plugins without creating the global instance #4158).defer. Onv2, the classic build creates a second instance and highlights the page again. Module plugins hook only into the first instance. Now the module copy puts its instance inglobalThis.Prism, and the classic build uses it.v2, a copy adopts anywindow.Prismwhose class is namedPrism, and its plugins register there. Now each major version keeps its own instance and registry, and warns. No v3 exists yet, so this case covers future pages.ComponentRegistrythat does not depend on thePrismclass, collects languages and plugins. The global instance takes everything from it throughregistry.subscribe(). An instance fromnew Prism()gets only what is passed toprism.register(def).highlight(),tokenize()and the rest) no longer fall back to the global instance withoutthis, so importing the class no longer creates it.new Prism()no longer highlights the page or reads page settings. Themanualconstructor option is gone, andnew Prism({ manual: true })has no effect: callhighlightAll()yourself.data-manual,data-prism-*andwindow.Prism = { … }now configure only the global instance.prismjs/global.js, the global instance without Autoloader and auto-highlighting, andprismjs/core.js, thePrismandTokenclasses only.Prism.isPrism(value)recognizes an instance from another copy of Prism, whereinstanceoffails. On a page, the first copy (classic or module) creates the global instance and puts it inglobalThis.Prism, with its registry asprism.registry. Later copies of the same major version use both, and only the creating copy highlights the page.Prism.versionholds the package version (devin unbuilt source). A copy of another major version creates its own instance with its own registry and warns, unlesssilentis set. It leavesglobalThis.Prismto that version, the classic build included.Not done: the website docs (new entries,
register(), the removedmanualoption). They get updated before the v2 release.Closes #4149
🤖 Generated with Claude Code