Solution: load independently compiled Webpack 2 bundles dynamically [closed] - javascript

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
This question does not appear to be about programming within the scope defined in the help center.
Closed 4 years ago.
Improve this question
I would like to share how to bundle an application that acts as a plugin host and how it can load installed plugins dynamically.
Both the application and the plugins are bundled with Webpack
The application and plugins are compiled and distributed independently.
There are several people on the net who are looking for a solution to this problem:
Multi-project build and dynamically loading modules with webpack
Loading prebuilt webpack bundles at runtime
How to expose objects from Webpack bundle and inject external libs into compiled bundle?
Dynamic Requires
https://github.com/webpack/webpack/issues/118
The solution described here is based on #sokra's Apr 17, 2014 comment on Webpack issue #118 and is slightly adapted in order to work with Webpack 2.
https://github.com/webpack/webpack/issues/118
Main points:
A plugin needs an ID (or "URI") by which it registers at the backend server, and which is unique to the application.
In order to avoid chunk/module ID collisions for every plugin, individual JSONP loader functions will be used for loading the plugin's chunks.
Loading a plugin is initiated by dynamically created <script> elements (instead of require()) and let the main application eventually consume the plugin's exports through a JSONP callback.
Note: You may find Webpack's "JSONP" wording misleading as actually no JSON is transferred but the plugin's Javascript wrapped in a "loader function". No padding takes place at server-side.
Building a plugin
A plugin's build configuration uses Webpack's output.library and output.libraryTarget options.
Example plugin configuration:
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, './dist'),
publicPath: '/' + pluginUri + '/',
filename: 'js/[name].js',
library: pluginIdent,
libraryTarget: 'jsonp'
},
...
}
It's up to the plugin developer to choose an unique ID (or "URI") for the plugin and make it available in the plugin configuration. Here I use the variable pluginURI:
// unique plugin ID (using dots for namespacing)
var pluginUri = 'com.companyX.pluginY'
For the library option you also have to specify an unique name for the plugin. Webpack will use this name when generating the JSONP loader functions. I derive the function name from the plugin URI:
// transform plugin URI into a valid function name
var pluginIdent = "_" + pluginUri.replace(/\./g, '_')
Note that when the library option is set Webpack derives a value for the output.jsonpFunction option automatically.
When building the plugin Webpack generates 3 distribution files:
dist/js/manifest.js
dist/js/vendor.js
dist/js/main.js
Note that vendor.js and main.js are wrapped in JSONP loader functions whose names are taken from output.jsonpFunction and output.library respectively.
Your backend server must serve the distribution files of each installed plugin. For example, my backend server serves the content of a plugin's dist/ directory under the plugin's URI as the 1st path component:
/com.companyX.pluginY/js/manifest.js
/com.companyX.pluginY/js/vendor.js
/com.companyX.pluginY/js/main.js
That's why publicPath is set to '/' + pluginUri + '/' in the example plugin config.
Note: The distribution files can be served as static resources. The backend server is not required to do any padding (the "P" in JSONP). The distribution files are "padded" by Webpack already at build time.
Loading plugins
The main application is supposed to retrieve the list of the installed plugin (URI)s from the backend server.
// retrieved from server
var pluginUris = [
'com.companyX.pluginX',
'com.companyX.pluginY',
'org.organizationX.pluginX',
]
Then load the plugins:
loadPlugins () {
pluginUris.forEach(pluginUri => loadPlugin(pluginUri, function (exports) {
// the exports of the plugin's main file are available in `exports`
}))
}
Now the application has access to the plugin's exports. At this point, the original problem of loading an independently compiled plugin is basically solved :-)
A plugin is loaded by loading its 3 chunks (manifest.js, vendor.js, main.js) in sequence. Once main.js is loaded the callback will be invoked.
function loadPlugin (pluginUri, mainCallback) {
installMainCallback(pluginUri, mainCallback)
loadPluginChunk(pluginUri, 'manifest', () =>
loadPluginChunk(pluginUri, 'vendor', () =>
loadPluginChunk(pluginUri, 'main')
)
)
}
Callback invocation works by defining a global function whose name equals output.library as in the plugin config. The application derives that name from the pluginUri (just like we did in the plugin config already).
function installMainCallback (pluginUri, mainCallback) {
var _pluginIdent = pluginIdent(pluginUri)
window[_pluginIdent] = function (exports) {
delete window[_pluginIdent]
mainCallback(exports)
}
}
A chunk is loaded by dynamically creating a <script> element:
function loadPluginChunk (pluginUri, name, callback) {
return loadScript(pluginChunk(pluginUri, name), callback)
}
function loadScript (url, callback) {
var script = document.createElement('script')
script.src = url
script.onload = function () {
document.head.removeChild(script)
callback && callback()
}
document.head.appendChild(script)
}
Helper:
function pluginIdent (pluginUri) {
return '_' + pluginUri.replace(/\./g, '_')
}
function pluginChunk (pluginUri, name) {
return '/' + pluginUri + '/js/' + name + '.js'
}

Related

Vue: How to build bundle for Nuxt with vue-cli-service?

A user tries to use my package for nuxt.js, but gets the error: document is not defined.
I found the first issue. When I build the bundle with "build-bundle": "vue-cli-service build --target lib --name index ./src/index.js",
vue-style-loader is being used. This, however, results in the error for using nuxt projects. This part is failing:
function addStyle (obj /* StyleObjectPart */) {
var update, remove
var styleElement = document.querySelector('style[' + ssrIdKey + '~="' + obj.id + '"]')
Document is not defined since we are using server rendering. But the question is how can I build up my package so that I can use it with nuxt?
I need:
index.common.js
index.umd.js
index.umd.min.js
This is due to the server-side rendering. If you need to specify that you want to import a resource only on the client-side, you need to use the process.client variable.
For example, in your .vue file:
if (process.client) {
require('external_library')
// do something
}
The above is the fundamental solution to document is not defined.
I checked some information and found that, this problem is not caused by your package. In fact, the problem lies on the cache-loader package in the user’s nuxt project.
For some reason cache-loader incorrectly determined the current environment as browser and not node so that vue-style-loader is confused and used client implementation instead.
So try to let users add the following configuration to the nuxt.config.js file to disable stylesheet caches on server-side:
build: {
...
cache: true,
extend(config, { isServer, isDev, isClient }) {
...
if (isServer) {
for (const rules of config.module.rules.filter(({ test }) =>
/\.((c|le|sa|sc)ss|styl.*)/.test(test.toString())
)) {
for (const rule of rules.oneOf || []) {
rule.use = rule.use.filter(
({ loader }) => loader !== 'cache-loader'
)
}
}
}
...
}
...
}
I found a solution but it is not using the vue-cli service. Instead, the files are compiled by rollup. I found using the cli service much easier. The only problem with the cli service is it will adjust the "flow" of your repo. However, you can modify the rollup.config.js to amend the folder structure.
The problem with rollup is that it isn't webpack. Therefore, all components using a webpack configuration need to be adjusted or rollup.config.js needs to be amended to include the additional functionality

RequireJS: apply plugin to module id dynamically

I'm using the requirejs-babel plugin which requires prepending 'es6!' to all module ids that need babel transpilation.
define(['es6!some-es6-module'], function(module) {
// ...
});
Is there an API in RequireJS that would allow me to inspect a module id and prepend the plugin id as-needed? For example, if I wanted to apply 'es6!' to all module ids in a specific directory?
Ultimately I need to be able to write defines like this define(['some-es6-module'], ...) and automatically add the es6! prefix depending on what the module id is.
Not looking for information on SystemJS or gulp tasks that do the transpilation ahead of time, etc.
The exact module ids are not known at configuration time- I just know in certain locations/directories, modules will need es6!.
Needs to work in the browser, at runtime
I am not 100% sure on your overall objective (do you want the es6 addition to module ID saved permanently or always auto-added?), but you may be able to use RequireJS mapping to substitute module ID's for defined modules. For example: -
requirejs.config({
map: {
// * - for all modules that require these, do this
'*': {
'some-es6-module': 'es6!some-es6-module'
}
}
});
However, considering your use-case you may need something more complicated than this, as mapping assumes you have actual different versions of files and is generally used for this purpose.
A more complicated solution I assume you are looking to avoid could be to dynamically loop your files before optimising them in r.js and loading/editing them via Node. It would get a little messy!
var config = requirejs.s.contexts._.config;
var needBabel = ['some-es6-module', 'another-module-name', 'another'];
for (var property in config.paths) {
if (config.paths.hasOwnProperty(property) && needBabel.indexOf(property) > -1) {
// load the module in node
// fs.readFileSync(__dirname + config.paths[property] + '.js');
// dynamically modify this file with text replacement
// save this file via Node again
}
}
// run Require JS optimiser
// undo everything you've just done when optimisation is complete
I ended up overriding the load method. The override uses the standard load for modules with mapped paths, otherwise it uses the es6 (requirejs-babel) plugin to load the module.
require.standardLoad = require.load;
require.load = function(context, moduleName, url) {
var config = requirejs.s.contexts._.config;
if (moduleName in config.paths) {
return require.standardLoad(context, moduleName, url);
}
require(['es6'], function(es6) {
es6.load(
moduleName,
require,
{
fromText: function(text) {
require.exec(text);
context.completeLoad(moduleName);
}
},
{});
});
};
Here it is in action: https://gist.run/?id=7542e061bc940cde506b

Webpack: unable to require automatically resolved dependencies using variables

While working on a Web app using Webpack to manage JavaScript dependencies, I stumbled upon the problem i'm going to describe.
Loading dependencies passing strings to require() works beautifully:
// main.js
var jQuery = require('jquery');
Here, jquery is installed with Bower, and Webpack is correctly configured to automatically resolve Bower modules.
Now, I'm working on the problem of conditionally loading modules, with particular regard to the situation where modules have to be downloaded from a CDN, or from the local server if the CDN fails. I use scriptjs to asynchronously load from the CDN, by the way. The code I'm writing is something like this:
var jQuery = undefined;
try {
jQuery = require('jquery-cdn');
} catch (e) {
console.log('Unable to load jQuery from CDN. Loading local version...');
require('script!jquery');
jQuery = window.jQuery;
}
// jQuery available here
and this code works beautifully as well.
Now, since I obviously have a lot of dependencies (Handlebars, Ember, etc.) that I want to try to load from a CDN first, this code starts to get a little redundant, so the most logical thing I try to do is to refactor it out into a function:
function loadModule(module, object) {
var lib = undefined;
try {
lib = require(module + '-cdn');
} catch (e) {
console.log('Cannot load ' + object + ' from CDN. Loading local version...');
require('script!' + module);
lib = window[object];
}
return lib;
}
var jQuery = loadModule('jquery', 'jQuery');
var Handlebars = loadModule('handlebars', 'Handlebars');
// etc...
The problem is that Webpack has a particular behaviour when dealing with expressions inside require statements, that hinders my attempts to load modules in the way described above. In particular, when using an expression inside require it
tries to include all files that are possible with your expression
The net effect is a huge pile of error messages when I try to run Webpack with the above code.
Though the linked resources suggest to explicitly declare the path of the JavaScript files to include, what I fail to get is how to do the same thing when I cannot, or don't want to, pass a precise path to require, but rather use the automatically resolved modules, as shown.
Thanks all
EDIT:
I still don't known how to use expressions to load those scripts, however, I designed a workaround. Basically, the idea is to explicitly write the require('script') inside a callback function, and then dinamically call that function when it's time. More precisely, I prepared a configuration file like this:
// config.js
'use strict';
module.exports = {
'lib': {
'jquery': {
'object': 'jQuery',
'dev': function() { require('script!jquery'); },
'dist': function() { return require('jquery-cdn'); },
'cdn': '//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js'
},
'handlebars': {
// ...
}
}
};
Inside my main code I, then, define an array of resources to load, like:
var config = require('./config.js');
var resources = [ config.lib.jquery, config.lib.handlebars, ... ];
And then when I have to load the development version, or the distribution version, I dinamically call:
// Inside some kind of cycle
// resource = resources[index]
try {
window[resource.object] = resource.dist();
} catch (e) {
console.log('Cannot load ' + resource.object + ' from CDN. Loading local version...');
resource.dev();
}
Here there's a more complete example of this in action.

Compiling dynamically required modules with Browserify

I am using Browserify to compile a large Node.js application into a single file (using options --bare and --ignore-missing [to avoid troubles with lib-cov in Express]). I have some code to dynamically load modules based on what is available in a directory:
var fs = require('fs'),
path = require('path');
fs.readdirSync(__dirname).forEach(function (file) {
if (file !== 'index.js' && fs.statSync(path.join(__dirname, file)).isFile()) {
module.exports[file.substring(0, file.length-3)] = require(path.join(__dirname, file));
}
});
I'm getting strange errors in my application where aribtrary text files are being loaded from the directory my compiled file is loaded in. I think it's because paths are no longer set correctly, and because Browserify won't be able to require() the correct files that are dynamically loaded like this.
Short of making a static index.js file, is there a preferred method of dynamically requiring a directory of modules that is out-of-the-box compatible with Browserify?
This plugin allows to require Glob patterns: require-globify
Then, with a little hack you can add all the files on compilation and not executing them:
// Hack to compile Glob files. Don´t call this function!
function ಠ_ಠ() {
require('views/**/*.js', { glob: true })
}
And, for example, you could require and execute a specific file when you need it :D
var homePage = require('views/'+currentView)
Browserify does not support dynamic requires - see GH issue 377.
The only method for dynamically requiring a directory I am aware of: a build step to list the directory files and write the "static" index.js file.
There's also the bulkify transform, as documented here:
https://github.com/chrisdavies/tech-thoughts/blob/master/browserify-include-directory.md
Basically, you can do this in your app.js or whatever:
var bulk = require('bulk-require');
// Require all of the scripts in the controllers directory
bulk(__dirname, ['controllers/**/*.js']);
And my gulpfile has something like this in it:
gulp.task('js', function () {
return gulp.src('./src/js/init.js')
.pipe(browserify({
transform: ['bulkify']
}))
.pipe(rename('app.js'))
.pipe(uglify())
.pipe(gulp.dest('./dest/js'));
});

Is it possible to stop requireJS from adding the .js file extension automatically?

I'm using requireJS to load scripts. It has this detail in the docs:
The path that is used for a module name should not include the .js
extension, since the path mapping could be for a directory.
In my app, I map all of my script files in a config path, because they're dynamically generated at runtime (my scripts start life as things like order.js but become things like order.min.b25a571965d02d9c54871b7636ca1c5e.js (this is a hash of the file contents, for cachebusting purposes).
In some cases, require will add a second .js extension to the end of these paths. Although I generate the dynamic paths on the server side and then populate the config path, I have to then write some extra javascript code to remove the .js extension from the problematic files.
Reading the requireJS docs, I really don't understand why you'd ever want the path mapping to be used for a directory. Does this mean it's possible to somehow load an entire directory's worth of files in one call? I don't get it.
Does anybody know if it's possible to just force require to stop adding .js to file paths so I don't have to hack around it?
thanks.
UPDATE: added some code samples as requested.
This is inside my HTML file (it's a Scala project so we can't write these variables directly into a .js file):
foo.js.modules = {
order : '#Static("javascripts/order.min.js")',
reqwest : 'http://5.foo.appspot.com/js/libs/reqwest',
bean : 'http://4.foo.appspot.com/js/libs/bean.min',
detect : 'order!http://4.foo.appspot.com/js/detect/detect.js',
images : 'order!http://4.foo.appspot.com/js/detect/images.js',
basicTemplate : '#Static("javascripts/libs/basicTemplate.min.js")',
trailExpander : '#Static("javascripts/libs/trailExpander.min.js")',
fetchDiscussion : '#Static("javascripts/libs/fetchDiscussion.min.js")'
mostPopular : '#Static("javascripts/libs/mostPopular.min.js")'
};
Then inside my main.js:
requirejs.config({
paths: foo.js.modules
});
require([foo.js.modules.detect, foo.js.modules.images, "bean"],
function(detect, images, bean) {
// do stuff
});
In the example above, I have to use the string "bean" (which refers to the require path) rather than my direct object (like the others use foo.js.modules.bar) otherwise I get the extra .js appended.
Hope this makes sense.
If you don't feel like adding a dependency on noext, you can also just append a dummy query string to the path to prevent the .js extension from being appended, as in:
require.config({
paths: {
'signalr-hubs': '/signalr/hubs?noext'
}
});
This is what the noext plugin does.
requirejs' noext plugin:
Load scripts without appending ".js" extension, useful for dynamic scripts...
Documentation
check the examples folder. All the info you probably need will be inside comments or on the example code itself.
Basic usage
Put the plugins inside the baseUrl folder (usually same folder as the main.js file) or create an alias to the plugin location:
require.config({
paths : {
//create alias to plugins (not needed if plugins are on the baseUrl)
async: 'lib/require/async',
font: 'lib/require/font',
goog: 'lib/require/goog',
image: 'lib/require/image',
json: 'lib/require/json',
noext: 'lib/require/noext',
mdown: 'lib/require/mdown',
propertyParser : 'lib/require/propertyParser',
markdownConverter : 'lib/Markdown.Converter'
}
});
//use plugins as if they were at baseUrl
define([
'image!awsum.jpg',
'json!data/foo.json',
'noext!js/bar.php',
'mdown!data/lorem_ipsum.md',
'async!http://maps.google.com/maps/api/js?sensor=false',
'goog!visualization,1,packages:[corechart,geochart]',
'goog!search,1',
'font!google,families:[Tangerine,Cantarell]'
], function(awsum, foo, bar, loremIpsum){
//all dependencies are loaded (including gmaps and other google apis)
}
);
I am using requirejs server side with node.js. The noext plugin does not work for me. I suspect this is because it tries to add ?noext to a url and we have filenames instead of urls serverside.
I need to name my files .njs or .model to separate them from static .js files. Hopefully the author will update requirejs to not force automatic .js file extension conventions on the users.
Meanwhile here is a quick patch to disable this behavior.
To apply this patch (against version 2.1.15 of node_modules/requirejs/bin/r.js) :
Save in a file called disableAutoExt.diff or whatever and open a terminal
cd path/to/node_modules/
patch -p1 < path/to/disableAutoExt.diff
add disableAutoExt: true, to your requirejs.config: requirejs.config({disableAutoExt: true,});
Now we can do require(["test/index.njs", ...] ... and get back to work.
Save this patch in disableAutoExt.diff :
--- mod/node_modules/requirejs/bin/r.js 2014-09-07 20:54:07.000000000 -0400
+++ node_modules/requirejs/bin/r.js 2014-12-11 09:33:21.000000000 -0500
## -1884,6 +1884,10 ##
//Delegates to req.load. Broken out as a separate function to
//allow overriding in the optimizer.
load: function (id, url) {
+ if (config.disableAutoExt && url.match(/\..*\.js$/)) {
+ url = url.replace(/\.js$/, '');
+ }
+
req.load(context, id, url);
},
The patch simply adds the following around line 1887 to node_modules/requirejs/bin/r.js:
if (config.disableAutoExt && url.match(/\..*\.js$/)) {
url = url.replace(/\.js$/, '');
}
UPDATE: Improved patch by moving url change deeper in the code so it no longer causes a hang after calling undef on a module. Needed undef because:
To disable caching of modules when developing with node.js add this to your main app file:
requirejs.onResourceLoad = function(context, map)
{
requirejs.undef(map.name);
};

Categories