Problem/Motivation
Drupal uses a new standalone library to provide the once functionality. This library is published on npmjs as the @drupal/once package. The package exposes a js module and a version compatible with browsers that don't support js modules (the "iife" version). This iife version exposes a global variable named "once", making window.once the entrypoint of the functionality, this is the version drupal core uses since IE11 support is necessary.
Some third party scripts/libraries can already declare a window.once that Drupal core ends up using instead of the intended @drupal/once script, causing errors in almost all behaviors since most of them use once.
Steps to reproduce
Add one of the following in some library definition that gets loaded on the page:
https://unpkg.com/alpinejs@3.x.x/dist/module.cjs.js: { attributes: { defer: true }, type: external, process: false }
https://cdn.flowplayer.com/releases/native/stable/plugins/chromecast.min.js: { attributes: { defer: true }, type: external, process: false }
Here the defer means those files will be loaded right before the behaviors are executed and after the once library has been loaded (so window.once will never point to the @drupal/once script).
Proposed resolution
None yet.
Remaining tasks
Find a good solution
Write tests
Original report by mrweiner
Problem/Motivation
Upon upgrading to 9.3, we are seeing a number of console errors for Uncaught TypeError: once(...).forEach is not a function from various modules including tour, toolbar, big_pipe, views, and contextual. One such example is ajax_view.js.
9.3.x, line 70: https://git.drupalcode.org/project/drupal/-/blob/9.3.x/core/modules/view...
once('exposed-form', this.$exposed_form).forEach($.proxy(this.attachExposedFormAjax, this));
Doing some logging, it looks like the calls are not always broken. When they work, once(...) appears to be an array. When they are broken, however, once(...) looks to be the Window, which is not iterable. I verified this in both ajax.js and big_pipe.js.
It seems that the culprit is that jquery.once.bc.js sets once() as a global via window.once, and if another script is loaded on the page that also sets this global then subsequent invocations of once() by core fire off this replaced version of the function, causing things to break.
Steps to reproduce
Two scripts identified so far to cause the issue are https://unpkg.com/alpinejs@3.x.x/dist/module.cjs.js and https://cdn.flowplayer.com/releases/native/stable/plugins/chromecast.min.js, although there are likely others as well.
To reproduce, create a new project using the "standard" profile and add a script which modifies window.once to core/once in core.libraries.yml (or, realistically, probably to any library) as in:
once:
remote: https://git.drupalcode.org/project/once
version: "1.0.1"
license:
name: GNU-GPL-2.0-or-later
url: https://git.drupalcode.org/project/once/-/raw/v1.0.1/LICENSE.md
gpl-compatible: true
js:
assets/vendor/once/once.min.js: { weight: -19, minified: true }
https://cdn.flowplayer.com/releases/native/stable/plugins/chromecast.min.js: { attributes: { defer: true }, type: external, process: false }
#or
https://unpkg.com/alpinejs@3.x.x/dist/module.cjs.js: { attributes: { defer: true }, type: external, process: false }
dependencies:
- core/drupal.element.matches
and then navigate to the homepage. Errors will be thrown by ajax.js and others.
Proposed resolution
Instead of setting window.once and calling to as a global, attach it to the Drupal global as Drupal.once or remove it from the global namespace by other means so as to avoid conflicts with third-party libraries.
| Comment | File | Size | Author |
|---|---|---|---|
| #68 | wait-untill-jquery-once-is-ready.patch | 1.03 KB | emircan erkul |
| #65 | 3254840-65.patch | 1.42 KB | _utsavsharma |
| #65 | interdiff_61-65.txt | 519 bytes | _utsavsharma |
| #61 | window-once-name-conflict-3254840.patch | 1.27 KB | robcarr |
| #57 | 3254840-57.patch | 4.23 KB | gauravvvv |
Issue fork drupal-3254840
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
mrweiner commentedComment #3
cilefen commentedI related this at #3183149: Deprecate jquery.once and use the new once lib.
Comment #4
cilefen commentedThe rough regex is to search for
once\((.*)\).forEach\(and replace withonce($1).each(, however the parent issue is really clear about forEach being the correct function.Comment #5
mrweiner commentedThanks for linking. You're right, it's pretty clear that forEach is intentional. It's possible that something is wrong with our env specifically, so I'd be interested to hear if anybody else is encountering this.
Comment #6
mrweiner commentedComment #7
mrweiner commentedThat regex did work, but actually did not fix the problem. Instead I started getting once().each is undefined instead, so something else is going on. Updated the issue description.
Comment #8
mrweiner commentedAlright, updated the issue again. This is not related specifically to
forEach(), but is happening because sometimesonce()appears to be set to theWindowwhich cannot be iterated withforEach().Comment #9
longwaveCan you reproduce this on a new install of Drupal? It's possible this is caused by a contrib module, custom module or a theme.
Comment #10
mrweiner commentedI was able to reproduce on a clean install by adding a flowplayer player that uses their asnyc embed code into a Full HTML block, and adding it to a page with big_pipe enabled.
I then added the this logging to ajax.js:
When it works correctly, the log line looks like
function augmentedOnce(id, selector, context)and clicking on it takes me to/core/misc/jquery.once.bc.js?v=9.3.0as expected. When it's broken, the log line looks likefunction once(e, n), and clicking on it takes me tohttps://cdn.flowplayer.com/releases/native/stable/plugins/chromecast.min.jsadded by the player.I think the issue is that
web/core/misc/jquery.once.bc.jsis attachingonce()to the window object, and callers are making the assumption that when they callonce(), thewindow.oncemethod defined by that file will be the one that is called. I suspect that if something else happens to overridewindow.oncethen the overridden version is called instead. I'm not sure what else would causeconsole.log(once)to change like that. I'd imagine that a solution to this would involve drupal's own once not being attached to the window and invoked globally, but instead attached somewhere where it will reliably be not overwritten.Unfortunately I'm not sure of a good way to to reproduce this without the flowplayer embed, although I'd imagine it must be possible. The demo embed does not load up if you implement it yourself, so we might need to figure out some other way to get a player to use for testing. I cannot provide embeds that we are using because the content belongs to my client. If needed, I could set up a trial account and provide an embed to use, although they're only free for 14 days.
Comment #11
droplet commentedCould you simply log `element` in
I can't reproduce it. Use a module or only HTML code?
EDIT:
above flowplayer example is outdated I think. Use below one, the player is shown but no errors
https://flowplayer.com/developers/player/player-api#api-retrieval
Comment #12
xjmPostponing for an answer to #11.
Comment #13
ericpughI'm having the same issue with a custom theme using AlpineJS. I'm also having a hard time coming up with a dead simple example to reproduce (because I'm using a bundler in the theme) but I can at least reproduce the errors by including the following module in the theme's libraries.yml:
With this addition I have about half a dozen
once(...).forEach is not a function. Most importantly, in big_pipe.js which doesn't render the content in the placeholder. Of course, if I add the library using a minified/terser version there's no longer a conflict. (But that's pretty bad for the dev environment).It seems likely that this might be an issue with a lot of packages.
Edit:
window.oncein devtools returns Alpine's "once" functionComment #14
mrweiner commentedRe: #11 and #12
As I said, "The demo embed does not load up if you implement it yourself, so we might need to figure out some other way to get a player to use for testing." My link is not outdated, but the snippet on their examples page does not cause the error.
No additional modules. This was a "standard" installation of drupal with big_pipe enabled, and I added the embed via "source" of the Full HTML textarea in a custom block. Here is a version of our embed with IDs removed, the format outlined by https://flowplayer.com/developers/player/player-api#creating-a-flowplaye......
I'll note that the player has the chromecast plugin enabled in our account backend, which I think is adding the chromecast script I noted from the logging.
#13 though seems to confirm that the problem is likely related to window.once being overridden and the subsequently invoked by Drupal.
Comment #15
mrweiner commentedActually, #13 is great -- makes this easily reproducible. Just update core/once in core.libraries.yml to the following and navigate to the blocks page at /admin/structure/block.
Errors are present and the logging I mentioned in #10 above points to alpine's modue.cjs.js once method instead of Drupal's.
Comment #16
droplet commentedI still can't reproduce it sadly. Maybe someone can help to do a test.
I combined with flowplayer example with #14:
** I removed `hls` because it doesn't played if I added.
I search all code from Chrome console. I think flowplayer scripts are scoped.
** BUT, what made me interested is you mentioned "big_pipe". A particular loading order may matter.
#13
According to your report, this may be a different issue. the same `once` function is overridden. I don't know if we should rename it or not. I can imagine it's a very common problem if we loaded different 3rd parties source code.
BTW, if you have not a loader, you can't load commonJS in browser, right? (it may still work...but ....) Why not use ESM version? Using a sourcemap version will help for debugging.
Comment #17
ericpughI'm using ES6 imports for AlpineJS in a custom theme. I posted the example with CommonJS trying to find a simple example to reproduce without downloading JS files, explaining a build setup, etc.
Comment #18
nod_so it's a conflict with the global name
once?in that case core can't do much about it, we would need to change the library global name and update all the file closures to something like
But that also means all contrib/projects will need to update, which is not realistic.
Comment #19
ericpughI see. I do appreciate the work being done to unwind JQuery from core.
Maybe there's a "feature request" here around utils in the global scope? Would it be possible to attach
oncetowindow.Drupal? (I'm really not knowledgeable enough about the internals here to make any real suggestions).Comment #20
mrweiner commentedWell, the conflict is when both core and some other third-party define a global
once, and core tries to call it after it's been modified by the third-party. Core can do something about this, because the break was introduced by core's addition of the newcore/onceandcore/jquery.once.bclibs. Potentially just the latter, actually, but I'm not 100% sure. Before 9.3, my flowplayer embed and presumably alpinejs both worked fine, but now they do not.jquery.once.bc.jsdoes this, which is at least part of the issue.It seems like the simplest thing would be to instead attach core's new
once()method to theDrupalglobal or something like that (like behaviors already are) where it is safe from modification instead of adding it right on thewindow. There are potentially a significant number of third-party libraries out there that will now cause core's own js logic to totally break just by being loaded by the page.Comment #21
mrweiner commentedHere's a simple proof of concept patch that does a find and replace from
once(toDrupal.once(with the space present to avoid targeting PHP and only hit the JS once calls), and also changeswindow.oncetoDrupal.once.For reproducable testing, simply update your drupal/once library in core.libraries.yml to the following (to include the "breaking" chromecast script I was encountering, without any alpinejs overhead), enable big_pipe, and visit the blocks admin page:
Before the patch, you should see the once(...).foreach errors, and afterward you should not. Marking as "needs review" to call our friendly neighborhood testbot, but there may be more that needs to be fixed as well.
Comment #22
mrweiner commentedMaybe somebody more familiar with the test runner knows what "custom commands failed" means?
Comment #23
longwaveClick through to the CI results at https://www.drupal.org/pift-ci-job/2268880 - we use prettier to enforce JS style, the checks for that are failing.
Comment #24
mrweiner commentedAh okay, that's what I thought but wasn't sure. Didn't realize prettier needed to be run before tests. For anybody's reference, instructions at https://www.drupal.org/node/2986680. I think this one should be better.
EDIT: D'oh, this also replaced
function oncein the once.js. Will probably need to fix that but can't do that right now.Comment #25
nod_Couple of issues with this approach
assets/vendor/oncefiles is not something we want to doComment #26
nod_Looking at the flowplayer script it adds the nodejs event API to the
windowanddocumentobjects to bind 2 event handlers (so only using .on)i'm inclined to say this is a flowplayer issue.
Comment #27
ericpugh... and an AlpineJS issue.
I understand that we're not going to be able to handle every issue with third party libraries, but I think it might be worth considering better ways to use a standalone JS packages. (Something like Vue's
Vue.use()plugins comes to mind).Sure. But if we thought this was going to continue to be an issue (and presumably there are plans to add more @drupal packages) it would probably be better to depreciate something sooner rather than later.
Final, in case anyone lands on this issue the (not great) work around seems to be to uglify the conflicting library. If anyone is specifically using AlpineJS I'm using rollup and terser in my theme.
Comment #28
mrweiner commentedUnderstood, and in fact I did not mean to patch the once library itself. I'll be creating a new patch that does not touch its files. That said, my understanding is as follows (and if I'm misunderstanding any of the internals, please let me know):
The once package itself may be totally fine. In standalone projects you are able to namespace your imports and potentially avoid what we are seeing here. However, the way that Drupal leverages the package is problematic in that we have not namespaced it, instead deciding to attach a commonly used function name to the global namespace in
jquery.once.bc.js. As long as callingonce()incore/misc/ajax.js(or anywhere else) invokeswindow.once, any other library that also setswindow.onceis very likely to break Drupal's logic through no fault of the end user, and indeed there is no way to work around that.Flowplayer and alpinejs should probably not be using the window object but, in the end, we probably should not be either. End-users could be pulling in any number of third-party libs and it seems odd that we'd be okay with breaking them when we actually don't need to.
Looking ahead, I agree with @ericpugh. There may be some short-term pain with asking contrib to make the updates a second time, but this seems to just have been an avoidable oversight with a potentially simple fix.
Further, existing sites that were working without issues (and theoretically should continue working, since this update was not intended to brick them) are already in trouble without a way forward. This feels potentially more problematic to me.
Comment #30
mrweiner commentedAlright, I created an MR for an updated version of the changes from #24 which:
core/assets, including the once package itself per #25$(once(...))that were missed by my initial find and replaceDrupalis actually passed into the closure injquery.once.bc.js. I think this was the cause of the one functional test failure in #24Comment #31
mrweiner commentedReverting issue title -- not sure what happened. Also removed "draft" from the MR title because I think it blocked tests?
Comment #32
mrweiner commentedUpdating the issue description to better reflect my current understanding of what's going on.
Comment #33
mrweiner commentedNeeds work per failures: https://dispatcher.drupalci.org/job/drupal_patches/106604/console
There are some "Drupal.once is not a function" errors, so I'd assume that the Drupal global needs to passed in somehow for those cases. Looks like other tests just didn't get their once() calls replaced by the find and replace, so it makes sense that those are failing. I don't know whether those would also need to have the Drupal object made available for this approach.
I think I'm going to leave as-is for now until an approach is agreed upon since it seems silly to dial in tests for a solution that may not be used.
Comment #34
mrweiner commentedSorry to spam this issue, but I think the once module itself is also to blame. I didn't realize that the minified version is just straight up setting
var once = .... I'd assume this decision was made for ease of use, but we are seeing the side-effects here. This and the use of window.once both seem problematic to me.Comment #35
nod_Setting as critical as there is no good workaround yet
Comment #36
nod_Comment #37
droplet commentedThe issues of the global conflict do not exist in above 2 examples.
To load above examples in a plain HTML file. Then run `window.once` returned `undefined`.
At least, they don't export to `window` explicitly. Then if you have a build process, it should be no problems. You can use import alias..etc
Note:
this is older version:
https://cdn.flowplayer.com/releases/native/stable/plugins/chromecast.min.js
this is what loaded on my side from their auto loader:
https://cdn.flowplayer.com/releases/native/3/stable/plugins/chromecast.m...
I agree the global vars conflict still can be a problem with other scripts..The question is how common the `window.once` is.
Comment #38
mrweiner commented@droplet I'm not sure how the conflict comes about from those 2 scripts, exactly, but adding them to a library as described does cause a conflict with core. Whether they are assigning to
window.onceor just setting a globalvar once, or whatever, I'm not sure. And I can't really speak to why the "old" version of the flowplayer script is being loaded on our site, but all I can say is that it is the one that they are serving to us with the embed.I guess my thought is just that we should avoid polluting the global namespace if possible, regardless of how common a function name is. Especially if we want to start splitting more functionality into individual
@drupalmodules, we are going to open ourselves up to additional conflicts with each new module (as @ericpugh mentioned).@drupalmodules could easily do something like the following and avoid the issue entirely:This would maintain the "iife" pattern and IE11 compatibility afaik, and adding methods, etc. to the
Drupalglobal is already a well-established pattern.Comment #39
mrweiner commentedActually, I think you could implement the above with minimal headache for "contrib that has already begun upgrading". You could include both the "old" and the "new" versions of the scripts in
core/oncelib for the time-being. The global and Drupal scoped versions shouldn't conflict with each other, so I think that would allow contrib's global implementations to keep working while allowing core to instead implementDrupal.once. Global once() could then be given its own deprecation schedule, be that 10.0.0 or otherwise.Comment #40
justafishI agree with this and I like the solution proposed to put it into the Drupal namespace
Comment #41
nod_Thanks for the feedback.
On my end I don't want to change the library name/exports/iife at this point, since it's not Drupal specific and it'd be counter productive to have a dependency on the Drupal object in a standalone package. the iife is for convinience since ppl should probably use the module version directly.
In any case what I could see working is
Drupal.onceDrupal.onceinstead ofoncedirectly.Drupal.onceinstead of once (a one line change): from((once) => {})(window.once);to((once) => {})(Drupal.once);That way there is no impact on people who already updated their code, and there is way to restore the once global variable even temporarily.
In the worst case we could even force the once global to be this script when we run the behaviors and restore it afterwards:
not great but would probably fix the problems described here without any code change needed anywhere else.
Comment #42
nod_Ok working workaround. Not pretty but should unblock some use cases, implemented what I talked about in #41.
This works for the flowplayer use-case.
About alpine not sure what to do. Can't replicate, using the cdn.js version doesn't trigger any errors and can't load the cjs version without some more files around that makes loading commonjs in the browser works.
Comment #43
droplet commented#41, #42 only worked for the particular scenario
For example, I have a script loaded before and have following code in my 3rd party script:
Even executing scripts via `Drupal.attachBehaviors` isn't a must IMO.
Drupal also included `js-cookies` This is big brother but I think `window.Cookies` is very common. We need to apply the same standard for all scripts then.
Only RENAME can sort of this problem. (BUT if someone going to complain about `window.Cookies`, no deals?)
Comment #44
mrweiner commentedThe following still reproduces the error (without the patch) on a standard drupal install from the blocks admin page, so I'd give it a try for your testing:
There are some other errors related to alpine, but both toolbar and bigpipe are throwing the once error. The other alpine errors are really irrelevant as long as the once errors shows up, since that's what demonstrates the conflict.
Sorry to say that it does not. I'm seeing less errors with the patch applied, but /admin/structure/blocks is still showing instances of the error from bigpipe.js line 50 with the patch applied. Seeing the same with the alpine script + patch. This is on a fresh, standard install.
Comment #45
nod_right I usually disable bigpipe so my patch won't work in that case. And like droplet said it could be used outside the behaviors.
Guess we don't have an easy solution here. What's not fun is that a bunch of core js files don't put the once in the file-closure so can't even be automated as much as I wanted.
Comment #46
nod_Tried to go this way to at least warn people of the problem but that doesn't work with the BC layer we add in jquery.once.bc file
I'm just trying to make sure that renaming once in all core scripts is the only way, because that's going to be a big change and I'd rather avoid it if at all possible :/
for the alpine lib I can reproduce (don't know what I was doing before), although I would say loading a commonjs module in a browser is not ideal especially when a native es module is also exported.
For the flowplayer I'm just sad about the conflict, in both case I don't see anything simpler than changing the namespace.
If we put stuff in Drupal, does it mean we'll want to do that for other libs? jquery, backbone, underscore, etc.? if that's the case might want to have a
Drupal.utilsnamespace or something to avoid adding everything toDrupal.Comment #47
mrweiner commentedYeah I feel ya. I totally understand why it's all set up how it is. It's just unfortunate.
Good point. Since jquery, etc, are already used as globals across the js ecosystem it may be alright to leave them there. But I can the see argument for consistency that if we put one lib in
Drupalmaybe they should all go there. I don't know that I'm familiar enough with all of the tooling to make a real argument one way or the other.Fwiw, core already commonly does like
Drupal.ajax, so it's not the end of the world to drop them right onDrupal, but if this is going to become a common approach then you're right that it might be a good idea to add something likeDrupal.utilsfor the sake of organization.Comment #48
mrweiner commentedJust want to drop a note in here that it'd be awesome to get a fix into 9.4.0 for this if possible since affected sites are stuck on 9.2.x until we figure out a fix, and 9.2 support is dropped with the release of 9.4.
Comment #49
droplet commentedI believe only 2 options:
1. RENAME
2. Also RENAME but put it under Drupal namespace
So, while waiting for answers from top maintainers, we can help to think about the NEW NAME.
of course, to me, that's option 3: Do nothing.
Comment #50
droplet commentedActually, that's another option...
to modify the CORE & Contributed code directly.. The most simple way is SEARCH & REPLACE. (or a module triggered before Drupal JS aggregation)
Or load a JS compiler to rewrite it.. (babel / esbuild or whatever)
this is the only solution for scalable/unknown projects.. (conflicts can exist on 2 contributed modules also)
For a single project level this is the best option, you even able to load diff config to minify the JS files.
Comment #53
robcarrPatch at 42 no longer applies. Still seeing "Uncaught TypeError: once is not a function" error with 9.5.8 vanilla site
Comment #54
robcarrQuick re-roll attached - not seeing Console error on 9.5.8
Comment #55
robcarrSorry - last patch wouldn't apply. Rerolled patch again
Comment #56
akram khantry to fixed CCF #54
Comment #57
gauravvvv commentedI have fixed the build failure and attached a patch with interdiff. please review
Comment #58
smustgrave commentedThere anyway to write a test for this?
Issue summary should be updated as well as it says proposed solution is still TBD
Comment #59
robcarrI'm seeing this problem in D10.1 too, and it's clearly a problem generated by other modules.
A lot of the JS has been removed in D10, so the patches above that people have put a lot of work in won't apply. With the caveat of 'I've absolutely no idea what I'm doing' the attached patch stopped all the console errors and - in my case - has allowed the modals for Views to pop-up again. It may have stopped something else really significant working, but I'll revisit if that's the case.
Comment #60
robcarrSorry - bad patch...
Comment #61
robcarrAnd again... so sorry
Comment #62
mrweiner commented@smustgrave issue summary needn't be updated because no actual decisions have been made regarding how this should be addressed, unfortunately.
Comment #63
emb03 commentedI am trying to make a custom form using ajax to add and delete items, simple, right? I am using Drupal 10.1 and running into major problems with not being able to use jquery.once function. How is one supposed to make a simple form using form_state for temporary storage? Are there any workarounds? I have tried everything even chat GPT is stumped :)
Comment #64
voleger#63 jquery.once is deprecated since drupal:9.2.x and is removed since drupal:10.0.0. See https://www.drupal.org/node/3158256
Comment #65
_utsavsharma commentedTried to fix minor CCF in #61.
Comment #67
emircan erkul commentedIn my case, $.fn.once was not ready when script initialized which causes all headache.
Attached file solved my issue. Yes i should find the root cause but couldn't. Probably order of the loading those libraries causes but effected areas was huge to handle in my case. Anyway it's just temporary, we soon upgrade to D10.
Comment #68
emircan erkul commentedSorry path was a bit wrong here is correct one