Skip to content

Import Prism without creating the global instance - #4150

Closed
DmitrySharabin wants to merge 7 commits into
v2from
class-registry-prototype
Closed

DmitrySharabin wants to merge 7 commits into
v2from
class-registry-prototype

Conversation

@DmitrySharabin

@DmitrySharabin DmitrySharabin commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

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 v2 does today:

  1. A page creates its own instance with import { Prism } from "prismjs" and new Prism({ manual: true }). On v2, the import also creates the global instance, and that instance highlights the page (Should plugins register themselves, and how do we import the Prism class without side effects? #4149). Now prismjs/core.js gives the class without creating an instance.
  2. Code imports a plugin for its own instance. On v2, the plugin also joins the global instance (Should plugins register themselves, and how do we import the Prism class without side effects? #4149). Now a plugin joins only the instances it is registered with (with Import plugins without creating the global instance #4158).
  3. A page loads the classic build, then imports prismjs. On v2, 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.
  4. A page loads the classic build, then a language or a plugin as a module script. On 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).
  5. A page loads the module build first and the classic build later, e.g. with defer. On v2, 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 in globalThis.Prism, and the classic build uses it.
  6. A page has copies of two major versions. On v2, a copy adopts any window.Prism whose class is named Prism, 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.
  • A registry module, an instance of ComponentRegistry that does not depend on the Prism class, collects languages and plugins. The global instance takes everything from it through registry.subscribe(). An instance from new Prism() gets only what is passed to prism.register(def).
  • The core functions (highlight(), tokenize() and the rest) no longer fall back to the global instance without this, so importing the class no longer creates it.
  • new Prism() no longer highlights the page or reads page settings. The manual constructor option is gone, and new Prism({ manual: true }) has no effect: call highlightAll() yourself.
  • data-manual, data-prism-* and window.Prism = { … } now configure only the global instance.
  • Two new entries: prismjs/global.js, the global instance without Autoloader and auto-highlighting, and prismjs/core.js, the Prism and Token classes only.
  • Prism.isPrism(value) recognizes an instance from another copy of Prism, where instanceof fails. On a page, the first copy (classic or module) creates the global instance and puts it in globalThis.Prism, with its registry as prism.registry. Later copies of the same major version use both, and only the creating copy highlights the page.
  • Prism.version holds the package version (dev in unbuilt source). A copy of another major version creates its own instance with its own registry and warns, unless silent is set. It leaves globalThis.Prism to that version, the classic build included.
src/registry.js            findSharedPrism()?.registry ?? new ComponentRegistry()   ← every language and plugin
└─ find-shared-prism.js    globalThis.Prism, if it is a Prism instance of a compatible version
   └─ version.js           version (from package.json at build time), isCompatible(): same major, dev matches any
src/core/prism.js          sharedPrism ?? new Prism(): publishes globalThis.Prism (DOM only), subscribes its own registry
src/auto-start.js          highlights the page only in the copy that created the instance
src/iife.js                classic-build entry: exports the instance in globalThis.Prism, else its own
<!-- Page settings for the global instance: preload languages and plugins, no error messages -->
<script>
	window.Prism = { languages: ["css"], plugins: ["line-numbers"], silent: true };
</script>
<script type="module" src="dist/index.js"></script>
// Isolated instance: nothing global is created, languages and plugins join it only through register()
import { Prism } from "prismjs/core.js";
import css from "prismjs/languages/css.js";

const prism = new Prism();
prism.register(css);
prism.highlight("a { color: red }", "css");

Not done: the website docs (new entries, register(), the removed manual option). They get updated before the v2 release.
Closes #4149

🤖 Generated with Claude Code

@netlify

netlify Bot commented Oct 7, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for dev-prismjs-com ready!

Name Link
🔨 Latest commit 0b01e6d
🔍 Latest deploy log https://app.netlify.com/projects/dev-prismjs-com/deploys/6aca793010e5ac00086e7ced
😎 Deploy Preview https://deploy-preview-4150--dev-prismjs-com.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@DmitrySharabin

Copy link
Copy Markdown
Member Author

@LeaVerou, a question about the this ?? singleton fallback this PR removes from highlight(), highlightAll(), highlightElement(), tokenize(), _matchGrammar() and resolve().

It came with d5d7a5a and acff03f, when these functions moved out of the Prism class. The fallback imports the global instance into every module that loads the class, so prismjs/core.js could not exist without it.

As far as I can tell, nothing calls these functions without this, and the package has never exported them. Did you mean them to become a standalone API, for example:

import { highlight } from "prismjs/core.js";
highlight("let a = 1", "javascript"); // uses the global instance

If so, I'd keep them instance-agnostic some other way, without importing the global instance. If not, removing the fallback keeps them as plain Prism methods.

@LeaVerou

LeaVerou commented Oct 7, 2026

Copy link
Copy Markdown
Member

@LeaVerou, a question about the this ?? singleton fallback this PR removes from highlight(), highlightAll(), highlightElement(), tokenize(), _matchGrammar() and resolve().

It came with d5d7a5a and acff03f, when these functions moved out of the Prism class. The fallback imports the global instance into every module that loads the class, so prismjs/core.js could not exist without it.

As far as I can tell, nothing calls these functions without this, and the package has never exported them. Did you mean them to become a standalone API, for example:

import { highlight } from "prismjs/core.js";
highlight("let a = 1", "javascript"); // uses the global instance

If so, I'd keep them instance-agnostic some other way, without importing the global instance. If not, removing the fallback keeps them as plain Prism methods.

Yes, but it's not a super high priority goal if it adds significant complexity. It's a nice to have.

@DmitrySharabin

Copy link
Copy Markdown
Member Author

@LeaVerou, a question about the this ?? singleton fallback this PR removes from highlight(), highlightAll(), highlightElement(), tokenize(), _matchGrammar() and resolve().
It came with d5d7a5a and acff03f, when these functions moved out of the Prism class. The fallback imports the global instance into every module that loads the class, so prismjs/core.js could not exist without it.
As far as I can tell, nothing calls these functions without this, and the package has never exported them. Did you mean them to become a standalone API, for example:

import { highlight } from "prismjs/core.js";
highlight("let a = 1", "javascript"); // uses the global instance

If so, I'd keep them instance-agnostic some other way, without importing the global instance. If not, removing the fallback keeps them as plain Prism methods.

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.

@LeaVerou

LeaVerou commented Oct 7, 2026

Copy link
Copy Markdown
Member

Oof, that's …not an acceptable tradeoff. We can't have every language self-adding to the singleton like this. 😕
I wonder if it would make sense to add them to some kind of registry, that the singleton then uses. Then we don't need to pull the entirety of the singleton into every language which makes the dependency graph very weird.

@DmitrySharabin
DmitrySharabin force-pushed the class-registry-prototype branch from 0d3b2e4 to 1fdc4c1 Compare October 7, 2026 20:07
@DmitrySharabin

DmitrySharabin commented Oct 7, 2026 •

Copy link
Copy Markdown
Member Author

I wonder if it would make sense to add them to some kind of registry, that the singleton then uses. Then we don't need to pull the entirety of the singleton into every language which makes the dependency graph very weird.

Agreed. I followed your advice and now languages and plugins add themselves to a registry that does not depend on the Prism class. It reuses the existing ComponentRegistry and lives at globalThis[Symbol.for("prismjs.registry")], so the IIFE build and the ESM and CommonJS copies of Prism share one. The global instance takes everything from it, and other instances stay opt-in through register().

A language now imports only the registry. The static Prism.register() and Prism.global are gone.

@DmitrySharabin
DmitrySharabin force-pushed the class-registry-prototype branch 2 times, most recently from abc33bc to a716195 Compare October 8, 2026 15:56
@DmitrySharabin
DmitrySharabin removed this pull request from stack #4152 October 8, 2026 15:56
@DmitrySharabin
DmitrySharabin added this pull request to stack #4157 October 8, 2026 15:57
@DmitrySharabin
DmitrySharabin removed this pull request from stack #4157 October 8, 2026 15:57
@DmitrySharabin
DmitrySharabin added this pull request to stack #4159 October 8, 2026 15:57
@DmitrySharabin DmitrySharabin changed the title Import Prism, languages and plugins without creating the global instance Import Prism without creating the global instance Oct 8, 2026
@LeaVerou

LeaVerou commented Oct 8, 2026

Copy link
Copy Markdown
Member

We should not be adding any globals, even if they are symbols (except for the IIFE build, obviously)

@LeaVerou LeaVerou left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 instance core/prism.js ends up with (new or reused) achieves the same thing with no global. Details on src/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 on keep_classnames surviving every downstream minifier, and it accepts any unrelated window.Prism with a same-named constructor. Needs an explicit brand or at least a duck-type check, used consistently by core/prism.js and config.js. Details on src/core/prism.js.
  • Double highlightAll() when the IIFE is loaded and prismjs is imported afterwards. The reused instance's ready has already settled, so auto-start fires immediately. Details on src/auto-start.js.

Should fix before merge

  • The new test only exercises the add event path, not the replay loop that handles the common "language imported before the instance exists" order, and it leaks into the process-wide registry.
  • manual is half removed: gone from PrismConfig, still present (untyped) on the global instance's config, and new 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.js and autoloader.js still call languageRegistry.add directly instead of register().
  • ComponentRegistry.load() with no path imports from the literal string "undefined…".

What is good

  • Dropping the singleton import from highlight, tokenize, _matchGrammar and resolve is 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 that new Prism() no longer reads page config.
  • The Plugin constructor change to registry.prism.register(def) is behavior-preserving and removes duplicated dispatch logic.

Generated by Claude Code

Comment thread src/registry.js Outdated
Comment thread src/core/prism.js Outdated
Comment thread src/auto-start.js Outdated
Comment thread tests/core/registry.js Outdated
Comment thread src/config.js Outdated
Comment thread package.json
Comment thread src/core/classes/component-registry.js
Comment thread src/core/classes/prism.js
@DmitrySharabin

DmitrySharabin commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

We should not be adding any globals, even if they are symbols (except for the IIFE build, obviously)

Done in f7609c4. Changes since the review:

  • src/registry.js is a plain module-local new ComponentRegistry(). src/core/prism.js subscribes its instance through the new ComponentRegistry#subscribe().
  • Prism.isPrism(value) replaces the constructor.name checks in src/core/prism.js and src/config.js.
  • The prismjs entry no longer highlights the page a second time after the classic build.
  • manual is a separate export of src/config.js, not part of the instance config.
  • load() without a path throws, and load-languages.js and the Autoloader call register().
  • tests/core/global.js tests the global wiring in fresh processes: both import orders, reuse of the classic instance, and the auto-start guard.

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.

@DmitrySharabin
DmitrySharabin force-pushed the class-registry-prototype branch 3 times, most recently from 731a6aa to 02de9b7 Compare October 9, 2026 14:40

LeaVerou commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

🤖 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 subscribe(), Prism.isPrism() as the brand (confirmed present in the built IIFE), the auto-start guard with fresh-process fixtures, manual as its own export, load() throwing without a path, and register() in Autoloader and loadLanguages.

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

A defer classic script placed after a module script runs after it, and an IIFE injected later by a widget runs after it too. In that order the module copy finds no globalThis.Prism, creates instance A and schedules its highlight. The IIFE then finds nothing either, since the module copy never publishes its instance, creates B and highlights too. Two instances, two passes, and a plugin imported as a module only ever hooks into A. The fixtures test exactly one order.

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:

  • Whichever copy evaluates first creates the instance and does globalThis.Prism ??= prism. Every later copy, IIFE or ESM, adopts it. A user's window.Prism = { manual: true } is read and then replaced, as in v1.
  • Only when a DOM exists. The IIFE is browser-only, so in Node and bundlers there is nothing to coordinate with and no global is set. CJS-plus-ESM in Node stays two instances, as today.
  • Expose the registry on the instance, and make registry.js do globalThis.Prism?.registry ?? new ComponentRegistry(). That also closes the "module language next to the IIFE" gap from the registry thread: every copy's languages land in the one registry the one instance subscribes to, and registry.js still imports only ComponentRegistry.
  • The auto-start guard becomes "did this copy create the instance". The creator highlights unless manual, adopters never do. Add the ESM-first order to the fixtures.

2. Prism.version

A static on the class in src/core/classes/prism.js, filled at build time through the existing data-insert rollup plugin: a version_placeholder entry in dataToInsert that returns package.json's version. No release hook needed, since prepack runs the build after release-it bumps the version. Unbuilt source should read as an obvious dev marker.

Use it to gate adoption: same major adopts, a different major creates its own instance and says so with a console.warn gated on config.silent. A warning, not an error: two instances is a degraded page, not a broken one. Treat the dev marker as compatible with anything so a source checkout next to a dist build still adopts. Side benefit: Autoloader can default its CDN path to the matching version.

3. Correction: there are no unrelated changes in this PR

An earlier version of this comment listed the Autoloader path rewrite, the Filter highlightAll fix and the website-rebuild workflow as changes riding along in this PR. They are not. They landed on v2 as #4162, #4161 and fb40631 after my first review, and showed up in my diff because I compared the two PR heads instead of the PR against its base. GitHub's file list for the PR has none of them; its only Autoloader change is the one-line switch to register(). Sorry for the noise.

4. Minor

  • src/config.js now uses any non-instance globalThis.Prism as the config object. A primitive there throws at import time, because in rejects primitives. A typeof === 'object' guard restores the old safety.

5. Bundlers

The repo has no bundler tests, so I packed this head with npm pack, installed it in a throwaway project and bundled four scenarios with esbuild and webpack 5 (Rollup is what builds Prism, so it is covered): prismjs/core.js plus a language and register(), prismjs/global.js the same way, prismjs (Autoloader included), and an ESM import next to a CJS require of prismjs/core.js. I then built and packed v2 base and ran the same scenarios against it, to tell what this PR changes from what was already there.

What works: all four highlight correctly in both bundlers. The core.js bundles contain none of config.js (no data-prism-, no currentScript) and set no global. The dual-package case yields two class copies, as expected, and isPrism recognizes the CJS copy's instance from the ESM copy, so the brand does its job.

Two problems, both present identically on v2 base, so neither is this PR's and neither should block it. They are worth their own issues:

  • The variable import() in ComponentRegistry.load() makes webpack bundle the whole package. webpack cannot resolve import(this.path + id + ".js"), so it builds a lazy context over everything under node_modules/prismjs/dist/: on this head, 685 modules referenced and 723 chunk files emitted for an import of prismjs/core.js alone, including the IIFE, the whole cjs/ tree and every language and plugin, with a "Critical dependency" warning per site. v2 base measures the same (734 chunk files, 5 warnings, a 269 KB dev bundle for the prismjs entry). It matters more now that core.js exists to be small. Verified fix: import(/* webpackIgnore: true */ /* @vite-ignore */ path) at the two sites (ComponentRegistry.load() and the Autoloader's import) drops the chunk files to zero, the warnings to zero, and the dev bundle from 261 KB to 36 KB. Terser currently strips all comments (format.comments: false), so the build needs to keep these two, for example with a regex on format.comments.
  • import.meta.url under webpack is a build-machine path. webpack 5 replaces it with a literal file:///…/node_modules/prismjs/dist/autoloader-….js from the build machine, so the Autoloader's default srcPath becomes a file:// directory and loadLanguages("css") fails with "Cannot find module". esbuild leaves import.meta.url alone, so there it resolves to the bundle's own directory, a web URL at least. This came with Find Autoloader's grammars path when Prism loads as a module #4162 on v2 and behaves the same there. A cheap guard: ignore the derived path when its protocol is not the page's and fall back to ./ as before, and the Autoloader README should say that bundled apps set srcPath or data-autoloader-path explicitly, since no default can be right for them.

Worth adding a small bundler smoke test to the suite: bundle prismjs/core.js with esbuild and assert the output has no data-prism- and no context modules, so the next variable import or config leak fails CI.

Follow-ups, not for this PR

  • Idempotent highlighting. highlightAll() should skip elements it already highlighted. The marker has to be an attribute on the element, since a WeakSet in one copy is invisible to another and does not survive reserialization. It should carry the language plus a cheap hash of the code as highlighted, so "already highlighted" means "this exact content in this language" and live editors that change the text and call highlightElement() keep working. Expose it as a force option: highlightElement() defaults to true, highlightAll() defaults to false. Nested tokens are not a concern, since highlightElement() reads textContent; Keep Markup already has drop-tokens for its case.
  • Plugins own their markers. When core skips re-tokenizing an unchanged element it still runs complete, the way the empty-code path already does, and each plugin dedupes against its natural marker. Line Numbers, Toolbar, Line Highlight and Command Line already do. The rest (File Highlight, JSONP Highlight, Previewers, Unescaped Markup, the toolbar buttons) need an audit.

Generated by Claude Code

DmitrySharabin and others added 3 commits October 10, 2026 15:26
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>
@DmitrySharabin

DmitrySharabin commented Oct 10, 2026 •

Copy link
Copy Markdown
Member Author

Done in 46796a9, 410ad2c, 4cb0378 and cff4b6b. Changes since your second review:

  • The first copy of Prism on a page, classic or module, creates the global instance and puts it in globalThis.Prism. It does so only when a DOM exists. Later copies use that instance, and only the creating copy highlights the page. A page config in window.Prism is read, then replaced.
  • The instance carries its registry as prism.registry, and only the creating copy subscribes it. src/registry.js takes it from globalThis.Prism when that holds an instance of the same major version, so a module language next to the classic build now reaches its instance. For that check it imports a small class-free module (src/find-shared-prism.js, which uses src/version.js).
  • Prism.version comes from package.json at build time. Unbuilt source has dev, which matches any version. A copy of another major version creates its own instance with its own registry and warns, unless silent is set. It leaves globalThis.Prism to that version, the classic build included.
  • src/config.js ignores a primitive in globalThis.Prism.
  • tests/core/global.js adds the module-first order, a language before the classic build, the first copy highlighting the page, Node without a global, another major version, and one subscription of the shared registry.

The PR body now shows the module layout.

One build change: the data-insert plugin now replaces placeholders in transform, not renderChunk. Rollup's tree-shaking read the source default 'dev' as a constant and dropped the major comparison from the classic build. The other placeholders build byte-identical.

The bundler problems and the idempotent-highlighting follow-up are in separate issues: #4165, #4166, #4167.

DmitrySharabin and others added 3 commits October 10, 2026 19:41
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>
@DmitrySharabin
DmitrySharabin force-pushed the class-registry-prototype branch from cff4b6b to 0b01e6d Compare October 10, 2026 17:43
@LeaVerou

Copy link
Copy Markdown
Member

Done in 46796a9, 410ad2c, 4cb0378 and cff4b6b. Changes since your second review:

  • The first copy of Prism on a page, classic or module, creates the global instance and puts it in globalThis.Prism. It does so only when a DOM exists. Later copies use that instance, and only the creating copy highlights the page. A page config in window.Prism is read, then replaced.

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.

  • The instance carries its registry as prism.registry, and only the creating copy subscribes it. src/registry.js takes it from globalThis.Prism when that holds an instance of the same major version, so a module language next to the classic build now reaches its instance. For that check it imports a small class-free module (src/find-shared-prism.js, which uses src/version.js).

Doesn't this mean that including a language now has to pull in the singleton again?

  • Prism.version comes from package.json at build time. Unbuilt source has dev, which matches any version. A copy of another major version creates its own instance with its own registry and warns, unless silent is set. It leaves globalThis.Prism to that version, the classic build included.

Unbuilt source is not compatible with everything though… But that's not super important.

  • src/config.js ignores a primitive in globalThis.Prism.

?


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?

@DmitrySharabin

Copy link
Copy Markdown
Member Author

Wait, what? We may not want a global instance at all. This should only happen if the IIFE build is included.

A module copy sets globalThis.Prism for one load order only. The module build loads first, and the classic build loads later, e.g. with defer or from a widget. Separate module graphs share nothing but globals and the DOM. Without the global, the classic build cannot find the instance of the module copy. It then creates a second instance and highlights the page again. Your second review asked to cover this order (item 1). It is case 5 in the description now.

If that order is not worth a global, I'll drop it. Then only the classic build sets globalThis.Prism, and every module copy uses the instance it finds there. A page that loads the classic build after the module build then gets two instances and two highlighting passes, as on v2.

Doesn't this mean that including a language now has to pull in the singleton again?

No. A language imports src/registry.js. That module imports ComponentRegistry and src/find-shared-prism.js, and neither imports the Prism class. findSharedPrism() reads globalThis.Prism and calls isPrism() on the class of that value.

Unbuilt source is not compatible with everything though…

Agreed. dev matches any version so that a source checkout next to a dist build shares one instance. I'll make dev match only dev.

  • src/config.js ignores a primitive in globalThis.Prism.

?

Nothing changes against v2 here. The first round of this PR replaced the constructor.name === "Object" check. After that change, window.Prism = 1 made the import throw, as your second review found (item 4). The typeof guard restores the v2 behavior.

I’m a little worried we're overengineering this and introducing a lot of complexity that is not warranted by the use cases.

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?

@LeaVerou

Copy link
Copy Markdown
Member

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 globalThis.Prism already exists (and whether it's compatible) and use that instead of creating a new one. The ESM version could also check that and use it as the singleton if it exists and is compatible without creating a global. That still ensures one singleton, without the ESM build leaking any globals.

Agreed. dev matches any version so that a source checkout next to a dist build shares one instance. I'll make dev match only dev.

What I was saying is that dev for e.g. v2 and dev for e.g. v10 might be completely incompatible. What you're suggesting doesn't fix that. I'm not actually sure what it fixes.

The description now lists the concrete cases.

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?

@DmitrySharabin
DmitrySharabin removed this pull request from stack #4159 October 14, 2026 12:11
@DmitrySharabin

Copy link
Copy Markdown
Member Author

Closing this in favor of #4168, which starts over from v2 with only what the reviews here asked for:

  • Languages and plugins add themselves to a registry that imports only ComponentRegistry, so importing them no longer pulls in the global instance.
  • Only the IIFE build creates globalThis.Prism. Module code never sets it, and reuses the IIFE build's instance when one exists.

Everything added for load orders nobody has reported is gone: module code publishing the global, Prism.version and the other-major checks, isPrism, and the defer IIFE after a module. #4156, #4158 and #4151 now build on #4168.

Thank you for the reviews, they shaped the much smaller version.

@DmitrySharabin
DmitrySharabin deleted the class-registry-prototype branch October 14, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Should plugins register themselves, and how do we import the Prism class without side effects?

2 participants