Problem/Motivation

Quite a few contrib modules (for example dropzonejs, Content Browser, Select2) have an optional dependency on the Libraries module. They do this do allow the library to placed in locations other than DRUPAL_ROOT/libraries/. For example, on drupal.org distributions wishing to including third party libraries have to use drush make. This places the library in the install profile directory.

Therefore you see a lot of code like:

/**
 * Implements hook_library_info_build().
 */
function dropzonejs_library_info_build() {
  $libraries = [];

  if (\Drupal::moduleHandler()->moduleExists('libraries')) {
    $exif_path = libraries_get_path('exif-js') . '/exif.js';
  }
  else {
    $exif_path = DRUPAL_ROOT . '/libraries/exif-js/exif.js';
  }

  if ($exif_found = file_exists($exif_path)) {

One major problem with this is that libraries_get_path() is deprecated and supposed to be removed in the Drupal 8 (!!!) version of the Libraries module. This has not come to pass but essentially all the modules that are doing something similar to the dropzonejs example above are only using the Libraries module for 1 reason and thats because it searches a defined set up of paths for libraries. This depends on two of its methods - libraries_get_path() and libraries_get_libraries(). Both of these are deprecated with the message:

 * @deprecated Will be removed before a stable Drupal 8 release. Please use the
 * new library load and managment concepts described at:
 * https://www.drupal.org/node/2170763

The link shows that the Libraries module doesn't really want to continue providing this type of functionality and instead was trying to be a registry and had far more expansive D8 ideas. It doesn't appear that these have borne fruit.

Proposed resolution

There's a lot of discussion about how Drupal should support modules and themes wanting to use other frontend libraries. See #2605130: Best practices for handling external libraries in Drupal 8/9 and 10 for instance.

This issue is not trying to solve the bigger problem.

The proposal here is to search the following paths:

  • PATH/TO/INSTALL_PROFILE/libraries
  • libraries
  • PATH/TO/SITE/libraries

when we encounter a library path like /libraries/dropzone/dist/min/dropzone.min.js. This will allow modules to drop Libraries module integration but maintain the support for third party libraries in multiple locations.

The above search locations are the same as libraries_get_libraries() apart from sites/all/libraries has been removed - and I think the last location found wins so PATH/TO/SITE/libraries > libraries > PATH/TO/INSTALL_PROFILE/libraries

Remaining tasks

User interface changes

None

API changes

None

Data model changes

None

Release notes snippet

Third party front-end libraries can now be placed in site and/or install profile-specific libraries directories, providing the same functionality as the contributed 'libraries' module.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

alexpott created an issue. See original summary.

alexpott’s picture

Issue summary: View changes
Status: Active » Needs review
FileSize
2.56 KB

So one thought is maybe we don't need to support @libraries at all. We could implement dynamic library resolution for anything that starts with /libraries/ as that is the generic dumping ground. Here's a sketch of what this looks like. I've also not included sites/all/libraries because that's legacy in the libraries module so why do it here?

This is a very rough sketch that works but I need to do:

  • Docs updates
  • Dependency injection
  • Tests
Berdir’s picture

  1. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -189,7 +190,12 @@ public function buildByExtension($extension) {
                   if ($source[1] !== '/') {
    -                $options['data'] = substr($source, 1);
    +                if (strpos($source, '/libraries/') === 0) {
    +                  $options['data'] = $this->findLibrary($source);
    

    That's a neat idea.

    It means sites can just stop installing libraries.module, and all those existing contrib modules will continue to work with a non-standard library location. And once they require an appropriate core version, they can just drop the libraries.module integration.

    Existing replacements should continue to work although I'll want to explicitly test that.

    Also, the additional file exists checks should be OK because that only happens on a cache miss.

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -235,6 +241,37 @@ public function buildByExtension($extension) {
    +    // If don't find anything return the original path without the leading
    +    // slash.
    +    return substr($path, 1);
    

    should we repeat the reason for why we do this from buildByExtension()?

What about the use case of checking for the existence of the library in hook_requirements()? I suppose to we could get the library definition and check the path in there? Or should we make this API public to allow more direct access?

alexpott’s picture

Title: Add support for @libraries » Add support for third party libraries in site specific and install profile specific libraries folders
Issue summary: View changes
FileSize
19.17 KB
18.55 KB

Here's a tweaked patch for #3 - thanks for the review @Berdir.

I've added tests and dependency injection. Going to look for documentation to update. Also updated issue summary to detail the current path of the patch.

I also think it is a good idea to add a requirements helper to check that a third party library is installed. Will work on that.

Status: Needs review » Needs work

The last submitted patch, 4: 3096648-4.patch, failed testing. View results

alexpott’s picture

Status: Needs work » Needs review
FileSize
1.83 KB
18.68 KB

Fixing some of the test fails which appear to be due to kernel tests not having an install profile.

alexpott’s picture

So thinking about how dropzonejs is using the libraries module. It uses it in three ways:

  1. Alter its dropzonejs library in its libraries.yml file to have the correct path.
  2. It has a hook_requirements where it validates that libraries_get_path('dropzone') . '/dist/min/dropzone.min.js'; exists
  3. And it also has a hook_library_info_build() implementation that adds an exif-js library if $exif_path = libraries_get_path('exif-js') . '/exif.js'; exists.

The patch currently covers the first use-case. But the current method added does not suit the other use cases yet (even if we made it public). I'm thinking that maybe a new service LocalExternalLibrary service might be the way to go. One that can tell you if an expected file exists in the valid libraries directory and also can help with the requirements logic. Then there's the thought the ideally maybe the LibraryDiscoveryParser could use this same service.

Still thinking on the best way of proceeding and covering the additional use-cases.

tstoeckler’s picture

Hey there ;-). Some thoughts from a Libraries API (non-)maintainer:

  • The Problem/Motivation is spot on in regards to Libraries API. @sun and I schemed some very ambitious plans at DrupalCon Munich (!) but I never managed to materialize those into anything remotely functional, so it doesn't make sense to hold out any hope at this point.
  • I am fine with granting maintainership to someone here to pursue a less ambitious version of Libraries API. Absolutely not opposed to solving this issue in core, but I thought I'd mention it.
  • libraries_get_path() was originally intended as an "extension" of drupal_get_path(), i.e. it was a shim for drupal_get_path('library', ...) which did (and does) not exist. Since the conceptual descendant of drupal_get_path() is ExtensionList it may (or may not!) make sense to have a real extension.list.library service, etc. Again, not at all opposed to the current patch in particular, just thought I'd bring that up as general context.
alexpott’s picture

Thanks for the comment @tstoeckler. I think given that core provides a libraries folder and this is functionality that many contrib modules want having it in core makes sense.

Here's a new with a libraries directory finder service so we can have the same functionality as libraries_get_path()

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Asset/LibrariesDirectoryFinder.php
    @@ -0,0 +1,107 @@
    +/**
    + * @todo
    + */
    +class LibrariesDirectoryFinder {
    +
    

    :)

  2. +++ b/core/lib/Drupal/Core/Asset/LibrariesDirectoryFinder.php
    @@ -0,0 +1,107 @@
    +
    +    // Remove the directories/ from the path so it can be replaced.
    +    $updated_path = substr($path, 10);
    +    foreach ($directories as $dir) {
    +      if (file_exists($this->root . '/' . $dir . $updated_path)) {
    +        return $dir . $updated_path;
    +      }
    +    }
    

    I'm not sure i understand why we require libraries/ when we then cut it off. Ah, I guess we have to cut it of somewhere?

    What if we leave out "libraries" from the $directories list, then we don't have to cut off?

  3. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -67,6 +76,11 @@ public function __construct($root, ModuleHandlerInterface $module_handler, Theme
         }
         $this->streamWrapperManager = $stream_wrapper_manager;
    +    if (!$libraries_directory_finder) {
    +      @trigger_error('Calling LibraryDiscoveryParser::__construct() without the $libraries_directory_finder argument is deprecated in drupal:8.9.0. The $libraries_directory_finder argument will be required in drupal:9.0.0. See https://www.drupal.org/node/TODO', E_USER_DEPRECATED);
    +      $libraries_directory_finder = \Drupal::service('library.libraries_directory_finder');
    +    }
    +    $this->librariesDirectoryFinder = $libraries_directory_finder;
       }
    

    Sure about 9.0, wouldn't this have to be 10 now?

andypost’s picture

alexpott’s picture

I think given that core provides a libraries folder

lol well it doesn't ... given #667058: Add a libraries folder with a README.txt in it to DRUPAL_ROOT but it has been the de facto standard for a very very long time.

alexpott’s picture

Berdir’s picture

This looks pretty good to me, I've tested it with dropzonejs and it works nicely and the simplification there is great: #3099836: Use new LibrariesDirectoryFinder

  1. +++ b/core/lib/Drupal/Core/Asset/LibrariesDirectoryFinder.php
    @@ -0,0 +1,100 @@
    +
    +/**
    + * Finds libraries that are required by contributed and custom code.
    + */
    +class LibrariesDirectoryFinder {
    

    Not sure if it should be LibrariesDirectoryFinder or just LibraryFinder? It actually "finds" specific files of a library, not a directory (although that would work too I suppose), so just LibraryFinder might work too?

    Also not quite sure about the contributed and custom code part.. is that even relevant? Core doesn't need it because it's allowed to include all JS code that it needs, but is that really relevant?

  2. +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryParser.php
    @@ -189,7 +203,15 @@ public function buildByExtension($extension) {
                   if ($source[1] !== '/') {
    -                $options['data'] = substr($source, 1);
    +                $source = substr($source, 1);
    +                // Non core provided libraries can be in multiple locations.
    +                if (strpos($source, 'libraries/') === 0) {
    +                  $path_to_source = $this->librariesDirectoryFinder->find(substr($source, 10));
    +                  if ($path_to_source) {
    +                    $source = $path_to_source;
    +                  }
    +                }
    +                $options['data'] = $source;
                   }
    

    One use case in dropzonejs.module is an optional library. Not sure how common that is and whether or not we want to support that. We could have a flag like optional: true and if set, we'd unset the library definition. But the new code is also fairly simple although it means the module still needs to implement a hook, but it's pretty simple.

    So probably this is good enough for now?

    Another thing is documentation. A change record is one thing, but I'm wondering where/how we'd exactly document this new feature? We don't really have a standard for this, hook_library_info_build() mentions that you can also define it in $module.libraries.yml, \Drupal\Core\Asset\LibraryDiscoveryParser::parseLibraryInfo() mentions editor.libraries.yml by example.

alexpott’s picture

@Berdir thanks for the review.
Re #14

  1. I considered LibrariesFinder and LibraryFinder when I wrote the patch. The reasons I went for LibrariesDirectoryFinder are:
    • that this is all about whether a file or folder is in specific libraries directory
    • It's not a library finder that's used for all libraries
    • It makes it clear that this is not for all of LibraryDiscovery

    I agree the name is slightly awkward but I think that's because the functionality is slightly awkward.

    I've tried to improve the code comment.

  2. I agree - i think it would be worth exploring automating the missing library files and optional libraries in a follow-up. So contrib modules only need to add a MODULE.libraries.yml file. But yeah what we have here is good enough for now.

Thanks for #3099836: Use new LibrariesDirectoryFinder that's really helpful to see the new API in action and it is great that it makes something simpler :)

Good point on documentation. For me this needs to be documented on \Drupal\Core\Asset\LibraryDiscoveryParser::parseLibraryInfo() as that's where we document all the keys in the *.libraries.yml files.

Berdir’s picture

Component: base system » asset library system
Status: Needs review » Reviewed & tested by the community

I think this looks good.

This does add a D10 deprecation, but it's only a constructor thing and I only found a single subclass that doesn't override the constructor: http://grep.xnddx.ru/search?text=extends%20LibraryDiscoveryParser. So the chances that anyone will see this are slim and definitely not with drupal-check or similar static analysis tools.

The issue summary and CR already explain quite well that this would be very useful to have already in 8.9. Many common modules optionally integrate with the libraries module only to support these dynamic paths. There are two parts to this, the main functionality here just works, so basically a site can just stop installing libraries.module and it will still work. As the patch in #3099836: Use new LibrariesDirectoryFinder shows, modules that have e.g. hook_requirements() checks need to be adjusted to use the new API and would therefore depend on 8.9, but it's not an API break.

We also have the +1 from the current libraries.module maintainer in #8. As @tstoeckler said himself, libraries.module is currently not really maintained and has a very unclear future.

Some of the relatively common D8 modules that would benefit from this:
* Slick/Blazy
* Dropzonejs
* Select2
* Shariff
* (Before 8.8 also paragraphs, but that library is now in core, so we can remove that once we require 8.8)
* There are 9 pages of results on http://grep.xnddx.ru/search?text=libraries_get_path&filename= (of course also finds the module itself, some are for php libraries and other stuff, but still, it's quite a lot)

catch’s picture

As a small API/site-builder facing addition I think this is probably still viable to commit to 8.9.x. The constructor deprecation is fine too.

I haven't reviewed the patch properly yet though.

catch’s picture

I considered LibrariesFinder and LibraryFinder when I wrote the patch. The reasons I went for LibrariesDirectoryFinder are:
that this is all about whether a file or folder is in specific libraries directory

One issue with this is that we support placing different libraries in different directories on the same install - i.e. Library A in sites/SITE/libraries, library B in /libraries, and library C in profiles/PROFILE/libraries. So it is finding library directories (i.e. for one library), not the libraries directories themselves. Would LibraryDirectoryFinder be more self-documenting maybe?

alexpott’s picture

+++ b/core/lib/Drupal/Core/Asset/LibrariesDirectoryFinder.php
@@ -0,0 +1,100 @@
+  public function find($path) {

We're finding which libraries directory something (either a file or a directory) something is in. Hence going for LibrariesDirectoryFinder. For me LibraryDirectoryFinder would document that we're finding a directory rather than which libraries directory something is in.

I agree that the naming is not great but I think all the other suggestions so far (LibraryDirectoryFinder, LibraryFinder and LibrariesFinder) are not better. Looking at the original method that we're replacing (libraries_get_path) maybe LibrariesPathFinder? I'm not sure that's any better though.

@catch in the example you give in #18 this class is being used to find which libraries directory each library is in - so I do think that LibrariesDirectoryFinder is still appropriate.

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
Issue tags: +Needs release manager review
FileSize
11.25 KB
15.81 KB

Discussed the naming with @catch and @Berdir. We settled on LibrariesDirectoryFileFinder because this goes well with the current description which is accurate - Finds files that are located in the supported 'libraries' directories.

I've renamed all the things accordingly.

One last decision is do we put this in Drupal 8.8.x - I would think so because it helps people prep for D9 but this is a release manager decision. And if so should we have an empty post update function to force a container rebuild to add the new service.

catch’s picture

To me this is a new (site-admin-facing) feature and should just go into 8.9.x

alexpott’s picture

@catch sure and as @Berdir pointed out contrib can detect whether the service exists and use that to avoid calling deprecated code. The one thing that this means is that they can't drop the dependency on the contrib libraries module and have 8.8 compatibility. Or rather they can but then it gets super complex for them manage. I think not doing this in 8.8.x does make it harder to have modules like dropzonejs ready for Drupal 9 because no one is going to port the libraries module to Drupal 9.

alexpott’s picture

Ah so most of the D8 code has stuff like if (\Drupal::moduleHandler()->moduleExists('libraries')) { so there's no real dependency on the libraries module. If it is there paragraphs dropzonejs etc use it... if not then they don't.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs change record

Exactly, my plan is to basically do this in contrib modules:

if (new service exists) {
use that API
}
elseif (library module exists) {
use library module API
}
else {
use default location
}

and eventually once a module then requires 8.9, we can clean it up, and require the new service and stop integrating with library module at all. For users, the switch should be transparent, in 8.8, they need the library module if they have it in a non-default location, once they update to 8.9 they can uninstall that and then update to 9.0 without it.

I updated my patch for dropzonejs with this approach: #3099836: Use new LibrariesDirectoryFinder. Seems to be working nicely and I think this is ready, @catch agreed on slack with the name. I also updated the change record with the new name and this pattern.

  • catch committed 1edf15f on 8.9.x
    Issue #3096648 by alexpott, Berdir, catch, tstoeckler: Add support for...
catch’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed
Issue tags: +8.9.0 highlights, +9.0.0 highlights

For users, the switch should be transparent, in 8.8, they need the library module if they have it in a non-default location, once they update to 8.9 they can uninstall that and then update to 9.0 without it.

So we want users to be able to do a direct update from 8.8 to 9.x too, I can see two ways to do this:

1. Uninstall libraries module immediately prior to doing the 9.0.x update
2. Libraries could potentially do a 9.x 'port' which is just an .info.yml so that sites can do a clean uninstall after upgrading.

Opened #3110305: Drupal 8.9.x deprecation signposting against libraries module for potentially doing that stub release, and documenting options on the project page.

Fixed this on commit:

FILE: ...upal/core/lib/Drupal/Core/Asset/LibrariesDirectoryFileFinder.php
----------------------------------------------------------------------
FOUND 0 ERRORS AND 1 WARNING AFFECTING 1 LINE
----------------------------------------------------------------------
 89 | WARNING | [x] Empty PHP statement detected: superfluous
    |         |     semi-colon.
----------------------------------------------------------------------
PHPCBF CAN FIX THE 1 MARKED SNIFF VIOLATIONS AUTOMATICALLY
----------------------------------------------------------------------

Committed 8dad334 and pushed to 9.0.x and 8.9.x. Thanks!

  • catch committed 5f07251 on 9.0.x
    Issue #3096648 by alexpott, Berdir, catch, tstoeckler: Add support for...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.