Seth Lutske
Seth Lutske

Reputation: 10792

Making a leaflet plugin available as an import or directly from html header

I built a plugin for leaflet (npm package), and I want it to be available either as an ES6 import, or as a script in the HTML header.

Desired Behavior

For example, I want both of these to be possible:

// As an npm package
import 'leaflet-arrowheads'
// From your HTML header
<script type="text/javascript" src="./leaflet.arrowheads.js"></sript>

My plugin has a dependency, leaflet geometryutil. So the code looks something like this:

import 'leaflet.geometryutil'

L.Polyline.include({
   ...my plugin code here
})

L.LayerGroup.include({
   ... a bit more code here
})

This works when using ES6 modules, where leaflet.geometryutil is installed as a dependency of my package. But when being used in the header of an HTML file, this obviously won't work. (Of course anyone trying to do this will have to have the leaflet.geometryutil script in their header somewhere before my plugin - this is explained clearly in my github readme.)

My current strategy has been to just have 2 github branches - one with the import statement, and one without. But I am trying to understand what I can do to merge these two branches and have a single piece of code that works in both situations. My 2 ideas so far have been these:

Use a dynamic import

Only import the geometryutil module if it's not already present in the Leaflet namespace:

if(!L.GeometryUtil){
   import('leaflet-geometryutil')
      .then( GeometryUtil => {
         console.log('all good')
      })
      .catch( err => throw(err) )
}

L.Polyline.include({ ..plugin code... })

This doesn't work because the import promise doesn't return until after my L.Polyline.include code runs, so any use of GeometryUtil inside my code returns a GeometryUtil is undefined. This works in the HTML header scenario where the author has a GeometryUtil script tag prior to my plugin tag, because the if statement return false and the import promise is not executed.

Is there another way to write a dynamic import statement so that the plugin code runs only after the import promise is complete, and is then properly imported into another module that the plugin user is authoring?

Use a build tool like webpack

It seems I might be able to accomplish my goal with webpack. Based on the webpack docs about authoring libraries, I could do something like

output: {
    path: './dist',
    filename: 'leaflet.arrowheads.js',
    library: 'arrowheads',
    libraryTarget:'umd'
}

But I am not really sure how to configure the output exactly. The docs explain that this will expose a global variable arrowheads, but that's not what's needed. The plugin I'm building doesn't really expose a new variable, but rather adds to the existing leaflet library.

How can I configure a webpack.config to build the plugin so that it 1: is available as an import (which itself imports its dependency leaflet.geometryutil), and also 2: is available as a script tag from an HTML header, which automatically alters the leaflet object L as intended?

Is there a third option I'm not thinking of?

Thanks for reading.

Upvotes: 3

Views: 6325

Answers (2)

ghybs
ghybs

Reputation: 53350

This is a common puzzle for frontend libraries.

It is true that webpack can offer some solutions, same for Rollup (which is the build engine used by Leaflet).

As for dynamic import, it works fine when you build an application and have the dynamic import managed by your build engine, but I would not rely on it when writing a library, for which you do not know if the environment / build engine used by the consumer project will support it.

In your case, you may even save the hassle of using a build engine and having to play trying to configure it properly.

First of all, you properly noticed (and followed the Leaflet convention) that most Leaflet plugins do not export anything to global scope or to an importer, but have the side effect of adding some behaviour within Leaflet's L namespace. As such, when using a build engine importer, we just have to do:

import "leaflet";
import "leaflet-arrowheads"; // nothing in curly braces

Therefore your library does not even need to export anything (i.e. you can get rid of the export default before your L.Polyline.include).

You might now start seeing a possible solution: have a common file for both import and HTML script, containing only your L.Polyline.include code; have a separate import entry file (i.e. what you reference in your package.json/main that imports your dependencies AND your common file. BTW you could even import Leaflet in that file!

package.json

{
   "main": "index.js" // npm convention, not even needed if your file is named "index.js"
}

index.js

import "leaflet";
import "leaflet.geometryutil";
import "./src/leaflet-arrowheads.js"; // common file

src/leaflet-arrowheads.js

L.Polyline.include() // etc.

Then you have a single common file to maintain, no need to use a separate branch, everything is published to npm, i.e. free CDN's (like unpkg, cdnjs and jsDelivr) will be able to directly serve your common file, ready for HTML script consumption!

The last thing to worry is your dependency: in the case of node style import, it is fine since your package lists it as dependency, therefore it will be installed with your package, and you properly import it in the index file.

But for HTML script, you still have to import the dependency "manually", i.e. you still have to tell the users of your library to include the dependency in their HTML scripts.

This can be avoided by bundling the dependency with your plugin, typically by introducing now a build engine. In that case, the usual practice is to build several versions: 1 with all dependencies already bundled, so that your library user needs only 1 import; 1 with only your code, letting your library user managing the import of the dependencies (typically so that they have control of version, can use them direclty as well without duplicating code, etc.). For this, the above index file is also what is used as the build entry point. Then you have to specify what not to include in the bundle: exclude Leaflet, and possibly Leaflet-geometryUtil.

Upvotes: 4

jdadwilson
jdadwilson

Reputation: 17

I do the following for my site... After the end 'body' tag include the following line <?php include_once("files_java/_java_loader.php");?> The _java_loader.php file is like this...

<?php
$script_Array = explode("/", $_SERVER['SCRIPT_FILENAME']);
$script_Name  = $script_Array[count($script_Array)-1];

// Load module specific scripts for each module
switch ($script_Name) {
    case 'maps.php':
    case 'map_all.php':
    case 'neighbors.php':
    case 'view_large_map.php':
         ?><script type="text/javascript" src="<?php echo SERVER_PATH; ?>files_java/mapper.js"></script><?php echo PHP_EOL;
         ?><script type="text/javascript" src="<?php echo SERVER_PATH; ?>files_java/initMaps.js"></script><?php echo PHP_EOL;
         ?><script type="text/javascript" src="<?php echo SERVER_PATH; ?>files_java/downloadXML.js"></script><?php echo PHP_EOL;
         ?><script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=AIzaSyAXPvUkWaeVfdwQjtlBZwlZND0-0bcYpZs" defer></script><?php echo PHP_EOL;
    break;
}?>

Thus, you can set specific files to load for specific modules. Above the $script_array statement include all the javascript links that are needed for all modules.

I use a similar structure for CSS loads also.

jdadwilson

Upvotes: 0

Related Questions