webpack + leaflet plugin(s) - javascript

I have a requireJs-based library, that exports object T, which holds Leaflet object L. And I have a Leaflet plugin (this, in my case, but valid for any other), which adds some functions to the global object L. And I have my module, where I want to use object T with extended L. Question: how to gracefully do it with Webpack? I could modify the plugin, to import L from the first library, but anyway don't understand how to plug it in to my module. And not sure that this is the best way.
P.S. I saw this thread, and this as well, but didn't find a good way there. I don't want to use additional < script> tags, and this is quite rambled to have statements like this:
import 'my.leflet.library';
import 'leaflet.plugin';
somewhere in the source - instead, I would prefer to add something to the configuration, that will extend L object right after 'my.leaflet.library' would be loaded, and any module, that would import 'my.leaflet.library' will have T with modified L. Is it possible?

This is my webpack config for Vue (vue.config.js), you can probably adapt it for your needs.
const webpack = require('webpack')
module.exports = {
configureWebpack: {
plugins: [
new webpack.ProvidePlugin({
L: 'leaflet',
'window.L': 'leaflet',
})
],
}
}

Related

import css file into es6 returns string instead of object

TL;DR
I'm importing a css file into a typescript module, but the import resolves to a string instead of an object. Can anyone tell me why I don't get an object??
Example
// preview.ts
import test from './src/assets/test.theme.css';
// also tried this:
// import * as test from './src/assets/test.theme.css';
console.log('typeof test: ', typeof test);
console.log(test);
Console output
Detailed explanation
Actually, I'm trying to set up a Storybook for my Angular12 component library.
In order to provide various themes, I want to use the #etchteam/storybook-addon-css-variables-theme plugin, which in its documentation refers to the inline loader syntax of Webpack.
import myTheme from '!!style-loader?injectType=lazyStyleTag!css-loader!./assets/my-theme.css';
When applying this to my code my browser console started to complain
Error: myTheme.use is not a function
During my research I recognized that the imported stylesheet is not an evaluated javascript object, but instead it is provided as a string containing the sourcecode generated by the style-loader.
I also recognized, that this issue is not specific to the style-loader, but also occurs for all other loaders, e.g. css-loader, raw-loader, etc.
This issue is also not related to inline loader syntax, as it also shows up with loaders being defined in a minimalistic webpack config.
Environment:
Angular 12
Webpack 5
Reproduction
I have set up a GIT repo reproducing the issue.
The readme file explains the repro and the issue.
I think you have mistake in your Webpack config. You have nested rules property, instead you should have use:
{
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
}
https://webpack.js.org/loaders/css-loader/
I'm sorry, but I have to revert my last statement. My issue has NOT been resolved by #Akxe's comment.
Now my import statement (import * as test from '...') resolves to an object, but it's still not correct.
I have set up a GIT Repo to reproduce the issue. The readme.md file explains the repro and the issue.
It looks like Webpack is not executing/evaluating the return value of the loader.
Btw. this is not just the case with the css-loader. The result stays the same for raw-loader, sass-loader, style-loader, etc.
My final goal is to lazily load my theme files into a storybook.
I try to follow the documentation of the #etchteam/storybook-addon-css-variables-> theme.
Finally I got my issue solved!
Analysis
The main issue here is the webpack configuration generated by the #angular-devkit/build-angular package. I was able to analyze it by debugging a fresh angular12 application (you can check it out here).
By setting a break-point at /node_modules/#angular-devkit/build-angular/src/utils/webpack-browser-config.js, function: generateWebpackConfig(...), I could inspect the final webpackConfig object in the debugger.
The relevant rule looks like this:
The important part here is the rule setting the module type to asset/source, instructing webpack not to evaluate the loader's result.
Solution concept 1: inline loader
With the help of alexander-kait and his great hints at this issue,
I was able to find an inline-loader syntax that overrides webpack's module declaration:
import Test from 'test.css.webpack[javascript/auto]!=!!!style-loader?injectType=lazyStyleTag!css-loader!./test.css';
console.log(typeof Test); // output: object
console.log(Test); // output: Object { use: () => void, unuse: () => void }
Test.use(); // this should usually be called by a theme switcher...
I'm not really sure about the url pattern here, as it seems to be an undocumented feature, but I assume that it's something like <query-pattern>.webpack[<module-type>]!=!<loaders><query>.
However, since this is an undocumented feature, I was rather reluctant to use it.
Solution concept 2: webpackConfig customization
Since I'm in a storybook context, I decided to customize the webpack configuration according to the storybook documentation.
My solution requires to set up a naming convention (e.g. *.theme.css).
// .storybook/main.js
module.exports = {
webpackFinal: async (config) => {
// exclude *.theme.css from the *.css ruleset
config.module.rules.find(rule => '.css'.match(rule.test)).exclude = [ /\.(?:theme\.css)$/i ];
// add a rule for *.theme.css
config.module.rules.push({
test: /\.(?:theme\.css)$/i,
use: [
{ loader: 'style-loader', options: { injectType: 'lazyStyleTag' } },
'css-loader',
],
});
},
};
With these rules in place, I can now simply do the following:
// preview.js
import LightTheme from './light.theme.css';
import DarkTheme from './dark.theme.css';
setupThemeSwitcher(LightTheme, DarkTheme);
Please note that the setupThemeSwitcher function is just pseudocode merely there for the example. In reality I'm using the #etchteam/storybook-addon-css-variables-theme addon...
I had a very similar issue with storybook and this extension, except l’m loading .scss files.
I simply adapted solution 2 to suit my .scss case and it works like a charm.
I couldn’t make solution 1 to work, but as stated, it sounds hacky whereas solution 2 is cleaner in my opinion.
Thanks a lot for sharing this solution, I was struggling for hours.

Using Leaflet or any js library with Typescript and webpack

Note: I have searched a lot but I couldn't find my answer.
I am trying to create a map library by extending leaflet. But I think my problem is with using js libraries in general. I am using webpack and ts-loader to create the output file.
I have installed the types #types/geojson and #types/leaflet and moved them to modules folder.
here is my simple Test1.ts file :
import * as L from './modules/#types/leaflet/index';
import { MyDataItem } from './MyDataItem ';
export class Test1 {
text: string;
data: MyDataItem ;
latLng: L.LatLng;
constructor(theName: string) {
this.data = { name: theName} as MyDataItem ;
}
public show() {
console.log(this.data.name);
}
public showLatLng() {
this.latLng = new L.LatLng(22, 55);
console.log(this.latLng);
}
}
module.exports = {
Test1: Test1
}
I set library:'lib' in my webpack output option.
now I call the following methods from my browser console:
> (new lib.Test1("aa")).show() // result: aa
> (new lib.Test1("aa")).showLatLng() // error : L.LatLng is not a constructor
// or if it set webpack optimization:minimize to true the error is: i.LatLng is not a constructor
I have added leaflet.js to the page so typeof(L.latlng) is "function" (latlng with small letters).
Question: obviously the types that I used from leaflet are not accessible after generating js files. My assumption was if you create ts declaration file (.d.ts) for any js library you can use that library in your ts files. what am I missing here? How to add and use normal js libraries in Typescript file so that they work after bundling them with webpack?
The usual practice is to install both leaflet and its associated #types/leaflet packages in your node_modules folder, and to just import * as L from "leaflet" (or just import L from "leaflet", depending on your webpack configuration). The types will be automatically visible. And webpack will automatically bundle Leaflet.
Same for all other JS libraries.
By importing just the types (import * as L from './modules/#types/leaflet/index';), you do tell TypeScript what L means, but not webpack.
If for some reason you really want to add the Leaflet library by yourself, instead of letting webpack bundling it with the rest of your application, then you have to tell webpack to leave L as it is, because you will provide it in global scope somehow. Configure webpack with externals, e.g.:
module.exports = {
//...
externals : {
"./modules/#types/leaflet/index": {
root: 'L' // indicates global variable
}
}
};

require('template.jade') in react-starter-kit

Can someone maybe explain me, how this build-time require works?
https://github.com/kriasoft/react-starter-kit/blob/feature/redux/src/server.js#L89
They are requiring a jade template, which package or configuration allows this, I seem unable to find it myself.
const template = require('./views/index.jade')
I think is much more elegant then:
import jade from 'jade'
const template = jade.compile('./views/index.jade')
As RGraham mentioned in his comment, the require call is being "intercepted" during webpack's compilation of the application bundle. This is done using "loaders" that define particular behaviour for imports of a particular type:
Loaders allow you to preprocess files as you require() or “load” them.
In this particular case, the loader that does this modification could be one of these (or another that I didn't find in my search):
https://github.com/bline/jade-html-loader
https://github.com/webpack/jade-loader
Edit: looking at the project's own webpack configuration we can see it is the second link above:
{
test: /\.jade$/,
loader: 'jade-loader',
}
jade-loader reads the content of the specified file, which make look something like this (Jade string):
h1 Hello, #{author}!
..and replaces that with a CommonJS JavaScript code similar to this (at compile time):
module.exports = function(data) {
return `<h1>Hello, ${data.name}</h1>`;
};

JavaScript Faux Pas - Let me put global libraries on the window?

I'm using RequireJS. I absolutely hate the double variable syntax of defining a dependency and passing it in as a variable in a callback function. I am therefore attempting the implement the 'Sugar' syntax available in RequireJS.
However, I only want to 'import' global libraries like Backbone, jQuery, Underscore and Marionette once. jQuery and Backbone obviously assign themselves to the Window object but Underscore and Marionette do not.
This is my main.js file:
require.config({
paths: {
"jquery" : "vendor/jquery.min",
"underscore": "vendor/underscore-min",
"backbone" : "vendor/backbone-min",
"marionette" : "vendor/marionette",
"app" : "app/app"
}
});
define(function(require, exports, module) {
// Make these libraries available globally
var jquery = require('jquery');
window.underscore = require('underscore');
var Backbone = require('backbone');
window.marionette = require('marionette');
// Require and start our own app
var app = require('app');
app.start();
});
This obviously stops me from having to import/require each of these core libraries into every subsequent module/component for my application. Taking my code from potentially this (app.js file):
define(function (require, exports, module) {
var jquery = require('jquery'),
underscore = require('underscore'),
Backbone = require('backbone'),
Marionette = require('marionette'),
// module specific libs
mymodule = require('../js/app/module'),
logger = require('../js/app/logger');
return {
start: function () {
var testview = new mymodule();
logger.logme();
}
}
});
To this (better app.js):
define(function (require, exports, module) {
var mymodule = require('../js/app/module'),
logger = require('../js/app/logger');
return {
start: function () {
var testview = new mymodule();
logger.logme();
}
}
});
Much cleaner IMO.
So thoughts? Criticisms? Are these going to play well together? (two already do it themselves - why not for the other two if they are core to the app).
In my head I don't think it will be a problem as long as I don't start hammering every module/component/library onto the global scope but I'm interested for someone more experienced to weigh in.
If I'm doing it wrong or there is a better way let me know!
Thanks
EDIT: After reading your comments, I realized your question has two components. One component is purely syntactical - and my original answer addresses a possible solution to that component. However, to the extent that your solution also incorporates an application design component, whereby dependencies of individual modules are defined at the application level, my answer is that is "bad practice." Dependencies should be defined at the module level (or lower) so that every element of your application is decouplable. This will be an enormous benefit in writing code that is (1) reusable, (2) testable, (3) refactorable. By defining your dependencies at the application level, you force yourself into loading the entire application simply to access a single module, for example, to run a test - and you inhibit rearranging modules or reuse of the same modules in other projects. If you do this, in the long run... you're gonna have a bad time.
Now, to the extent that your question is syntactical...
ORIGINAL ANSWER: I agree that require.js syntax is ugly as hell, annoying to use, and a potential source of difficult to debug errors. My own approach was to implement the following wrapping function (pardon the coffeescript).
# Takes dependencies in the form of a hash of arguments with the format
# { path: "name"}
# Assigns each dependency to a wrapper variable (by default "deps"),
# without the need to list each dependency as an argument of the function.
PrettyRequire = (argHash, callback) ->
deps = new Array()
args = new Array()
#loops through the passed argument, sorts into two arrays.
for propt of argHash
deps.push(propt)
args.push(argHash[propt])
#calls define using the 'dependency' array
define(deps, ()->
deps = new Array()
# assigns the resulting dependencies to properties of the deps object
for arg, i in args
deps[arg] = arguments[i]
# runs callback, passing in 'deps' object
return callback(deps)
)
This code is a simple rewrite which (1) preserves scope and (2) prettifies the syntax. I simply include the function as a part of a internal library that I maintain, and include that library at the outset of any project.
Define can then be called with the following (imho) prettier syntax:
PrettyRequire({
'backbone': 'Backbone'
'vendor/marionette': 'Marionette'
'../js/app/module': 'myModule'
}, (deps)->
Backbone.Model.extend(...) # for dependencies that assign themselves to global
deps.myModule(...) # for all dependencies (including globals)
)
That's my approach, to the extent that it's responsive to your question. It's worth noting also that your apps will take a (small) performance hit as a result of the increased overhead, but as long as you're not calling too many sub modules, it shouldn't be too much of an issue, and to me, it's worth it not to have to deal with the double syntax you describe.

How to prevent moment.js from loading locales with webpack?

Is there any way you can stop moment.js from loading all the locales (I just need English) when you're using webpack? I'm looking at the source and it seems that if hasModule is defined, which it is for webpack, then it always tries to require() every locale. I'm pretty sure this needs a pull request to fix. But is there any way we can fix this with the webpack config?
Here is my webpack config to load momentjs:
resolve: {
alias: {
moment: path.join(__dirname, "src/lib/bower/moment/moment.js")
},
},
Then anywhere I need it, I just do require('moment'). This works but it's adding about 250 kB of unneeded language files to my bundle. Also I'm using the bower version of momentjs and gulp.
Also if this can't be fixed by the webpack config here is a link to the function where it loads the locales. I tried adding && module.exports.loadLocales to the if statement but I guess webpack doesn't actually work in a way where that would work. It just requires no matter what. I think it uses a regex now so I don't really know how you would even go about fixing it.
The code require('./locale/' + name) can use every file in the locale dir. So webpack includes every file as module in your bundle. It cannot know which language you are using.
There are two plugins that are useful to give webpack more information about which module should be included in your bundle: ContextReplacementPlugin and IgnorePlugin.
require('./locale/' + name) is called a context (a require which contains an expression). webpack infers some information from this code fragment: A directory and a regular expression. Here: directory = ".../moment/locale" regular expression = /^.*$/. So by default every file in the locale directory is included.
The ContextReplacementPlugin allows to override the inferred information i.e. provide a new regular expression (to choose the languages you want to include).
Another approach is to ignore the require with the IgnorePlugin.
Here is an example:
var webpack = require("webpack");
module.exports = {
// ...
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /de|fr|hu/)
// new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
};
In our project, I include moment like this: import moment from 'moment/src/moment'; and that seems to do the trick. Our usage of moment is very simple though, so I'm not sure if there will be any inconsistencies with the SDK. I think this works because WebPack doesn't know how to find the locale files statically, so you get a warning (that's easy to hide by adding an empty folder at moment/src/lib/locale/locale) but no locale includes.
UPDATE: 2021
There are many other libs that you may want to checkout:
https://date-fns.org
https://github.com/iamkun/dayjs
ORIGINAL ANSWER:
Seems like the proper modular moment library will never come up However, I just ended up of using https://github.com/ksloan/moment-mini like import * as moment from 'moment-mini';
As of moment 2.18, all locales are bundled together with the core library (see this GitHub issue).
The resourceRegExp parameter passed to IgnorePlugin is not tested against the resolved file names or absolute module names being imported or required, but rather against the string passed to require or import within the source code where the import is taking place. For example, if you're trying to exclude node_modules/moment/locale/*.js, this won't work:
new webpack.IgnorePlugin({ resourceRegExp: /moment\/locale\// });
Rather, because moment imports with this code:
require('./locale/' + name);
your first regexp must match that './locale/' string. The second contextRegExp parameter is then used to select specific directories from where the import took place. The following will cause those locale files to be ignored:
plugins:[
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
]
which means "any require statement matching './locale' from any directories ending with 'moment' will be ignored.
Based on Adam McCrmick's answer, you were close, change your alias to:
resolve: {
alias: {
moment: 'moment/src/moment'
},
},
With webpack2 and recent versions of moment you can do:
import {fn as moment} from 'moment'
And then in webpack.config.js you do:
resolve: {
packageMains: ['jsnext:main', 'main']
}
Here's another solution using postinstall script in NPM installer.
You can put a line to your package.json file:
{
"scripts": {
...
"postinstall": "find node_modules/moment/locale -type f -not -name 'en-gb.js' -not -name 'pl.js' -printf '%p\\n' | xargs rm"
...
}
}
In result unwanted locales will be removed immediately after npm install finish installing packages.
In my case only en-gb and pl locales will remain in bundle.
In case you already have postinstall script, you can add script to existing commands:
{
"scripts": {
...
"postinstall": "previous_command && find node_modules/moment/locale -type f -not -name 'en-gb.js' -not -name 'pl.js' -printf '%p\\n' | xargs rm"
...
}
}

Categories

Resources