Webpack: load js modules dynamically from a URL at runtime - javascript

I am currently working on a library which should dynamically load a JavaScript from a remote host and instantiate that.
I am writing the library in TypeScript and my plan is to use Webpack as a bundler.
On another host (remote system) runs a provider which should serve JavaScript code (see here: https://stubs.d-koppenhagen.de/stubs/SimpleStub.js).
The library will dynamically resolve "Identitys" via Webfinger. These Identitys represented by an object and they having a property pointing to a "Stub Provider" which will serve JavaScript code (the link I mentioned before). My library should load this script during runtime (s the library don't know the target for this stubs before) and should use it.
currently my webpack.config.js looks like the following:
var path = require('path');
var webpack = require('webpack');
var WebpackBuildNotifierPlugin = require('webpack-build-notifier');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const PATHS = {
src: path.join(__dirname, './src'),
build: path.join(__dirname, './dist')
};
module.exports = {
entry: {
wonder: PATHS.src + '/wonder'
},
output: {
path: PATHS.build,
filename: '[name].js',
library: 'Wonder',
libraryTarget: 'umd'
},
devtool: 'source-map',
module: {
loaders: [
{
test: /\.ts$/,
loader: 'ts-loader'
}
]
},
resolve: {
extensions: ['.ts', '.js']
},
plugins: [
new WebpackBuildNotifierPlugin()
]
};
And here is a part of the library code:
require.ensure([], function() {
require(localMsgStubUrl);
});
When I am now including the bundled library in an example app, I will get the following error:
Error: loading chunk failed
So is there a way to tell webpack not to bundle the code which is required from a external resource loaded from a URL so that I can use that code like it is?
I don't want to tell webpack the URL statically in a config as maybe other stubs I am loading are located on a different target.
Thanks in advance for your help!

Related

Importing a Javascript with Raw-Loader and alias with Webpack

I have a TypeScript project (Project A) which uses Webpack to create a bundle with target node.
This application needs to consume a Javascript file which is the output bundle of another project (Project B). This project emits, always via Webpack, a Javascript library for the browser with target web.
However, Project A needs to import this Javascript file raw, just as a string. It needs the content of the file, it does not need to access its types and functions.
Project structure
The webpack.config.js in Project A is as follows>
const path = require("path");
module.exports = {
target: "node",
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.js$/,
use: "raw-loader",
},
],
},
resolve: {
alias: {
Jsfile$: path.resolve(__dirname, "../projb/dist/bundle.js"),
},
extensions: [".ts", ".js", "..."],
},
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
library: {
name: "bundle",
type: "umd",
},
globalObject: "this",
},
};
Project B build script instructs Webpack to create a bundle with target web to be placed inside projb/dist/bundle.js.
As you can see, I try to use the raw-loader in Project A and I use an alias.
The problem
The file I have in Project A which is trying to import the JS file is:
import jsfile from "Jsfile";
But it is not working, giving me error:
ERROR in C:\Users\myuser\Documents\myproj\proja\src\file_importer.ts
./src/file_importer.ts 3:24-34
[tsl] ERROR in C:\Users\myuser\Documents\myproj\proja\src\file_importer.ts(3,25)
TS2307: Cannot find module 'Jsfile' or its corresponding type declarations.
What am I doing wrong?

Webpack Cannot load pdf file: Module parse failed You may need an appropriate loader to handle this file type

I am new to webpack. Recently, I wanted to find a way to view pdfs in one of my projects. I have found out a library called "react-pdf" (https://www.npmjs.com/package/react-pdf) and decided to experiment with it. When using it in a test react project (npx create-react-app), everything worked fine, but when I to introduced it to the real project, using Nextjs, things went wrong.
import placeholder from "../src/placeholder.pdf""
Every time the server reloads I get this error:
*
error - ./assets/pdf/placeholder.pdf
Module parse failed: Unexpected token (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
(Source code omitted for this binary file)*
This is the webpack config:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.pdf$/i,
type: 'asset/resource',
generator: {
filename: `[name][ext]`
}
}
],
},
mode: 'development'
};
What am I doing wrong?
next.config.js
module.exports = {
webpack: (config, options) =>
{
config.module.rules.push({
test: /\.pdf$/i,
type: 'asset/source'
})
return config
},
}

Creating a bookmarklet using webpack, bookmarklet-loader, style and css-loader

I am trying to create a bookmarklet using bookmarklet-loader and the style-loader and css-loader. But I am having trouble importing css into my bookmarklet.
This is what I have
webpack.config.js:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
index: './src/index.js',
bookmarklet: './src/bookmarklets/bookmarklet.js'
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /\.js$/,
use: [
'bookmarklet-loader'
],
include: path.join(__dirname, './src/bookmarklets')
}
]
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Development'
})
]
src/bookmarklets/bookmarklet.js:
import './css/style.css';
/* the rest of my bookmarklet code */
src/index.js:
import bookmarklet from './bookmarklets/bookmarklet';
var add = document.createElement("a");
add.href = "javascript:" + bookmarklet;
add.innerHTML = "Click me";
document.body.appendChild(add);
Simply adds the bookmarklet to a link on a blank page, so I can add the link to my browser.
But running webpack produces this error:
SyntaxError: Unexpected token: string (./css/style.css) at [snipped] node_modules/uglify-js/tools/node.js
I tried adding the following to my webpack.config.js:
{
test: /\.js$/,
use: [
'bookmarklet-loader',
'style-loader',
'css-loader'
],
include: path.join(__dirname, './src/bookmarklets')
}
This now compiles fine, but the bookmarklet code contains require statements so when I try and run it in the browser I get an
Uncaught ReferenceError: require is not defined
I have found this and this but have been unable to get this to work.
Edit:
To explain simply the question and solution. I am trying to build a bookmarklet, but the bookmarklet-loader I am using is used for importing bookmarklets into other pieces of code. And this bookmarklet-loader in particular is not setup to handle css and templates required by the bookmarklet. I have switched to using a simple webpack config that produces a compiled javascript file and then this tool to convert that to a bookmarklet.
This is my package.json in case if its of help to anyone:
<snip>
"scripts": {
"build": "webpack && bookmarklet dist/index.js dist/bookmarklet.js && cat dist/bookmarklet.js | xclip -selection clipboard",
}
Now npm run build builds the bookmarklet and copies it to my clipboard so I can update the bookmarklet in the browser.
I've also found this question interesting so here's an answer that would still let you use webpack for bundling your bookmarklet code.
The idea is to use a <script> tag and serve the content as a chunk through webpack:
function addScript(codeURL) {
const scriptElement = document.createElement('script');
scriptElement.setAttribute('src', codeURL);
scriptElement.setAttribute('crossorigin', "anonymous");
document.body.appendChild(scriptElement);
}
With some aditional 'magic', your index.js becomes:
const add = document.createElement("a");
add.href = "javascript:(function(){s=document.createElement('script');s.type='text/javascript';s.src='bookmarklet.bundle.js';document.body.appendChild(s);})()";
add.innerHTML = "Click me";
which is the uglified version of the above function that references your 'bookmarklet.bundle.js' chunk. (this way you don't really need the bookmarklet-loader any more)
The bookmarklet.js source (just a sample):
import './css/style.css';
let elements = require('./someOtherSource');
let list = document.createElement('ul');
for (let i = 0; i < elements.length; ++i) {
let item = document.createElement('li');
item.appendChild(document.createTextNode(elements[i]));
list.appendChild(item);
}
document.body.appendChild(list);
where someOtherSource.js could be as simple as:
module.exports = [ 'a', 'b', 'c'];
and finally, your webpack.config.js becomes:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
module.exports = {
entry: {
index: path.resolve(__dirname, 'src/index.js'),
bookmarklet: path.resolve(__dirname, 'src/bookmarklets/bookmarklet.js'),
},
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
]
},
{
test: /\.js$/,
use: [
'babel-loader',
],
exclude: /node_modules/,
}
]
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
title: 'Bookmarklet',
chunks: [ "index" ],
})
]
};
Again, the advantage I see here is that you get to use your webpack bundling, css/less or whatever other loaders for building your bookmarklet. As reference also see first and second.
The solution you detail in your edit is indeed a perfectly valid way of achieving your objective.
You want to maintain a bookmarklet that depends on injecting styles.
While you can easily inject tags (like <link> and <script>) with a bookmarklet to load external resources into the current page, it does not seem to fit your need because you do not need to make your code available on a server, and trying to link local resources on your file system might not be very reliable.
Therefore you would like the entire code and styles to be contained within the bookmarklet code. You can proceed in 2 steps:
Bundle your JS code with code for inline CSS injection + CSS
Encode and wrap the bundle so that its content can be used as a bookmark.
1. Bundle JS with code for inline CSS injection
This sounds like a perfect job for webpack! Indeed it is meant to bundle your code and inline your styles within the code as well, with style-loader like you did.
You could even push it slightly further by making sure any other asset (image, web font, etc.) that is potentially referred to in your CSS is also inlined, using url-loader with a limit: 0 to always inline those resources.
But as you figured out, you should not use the intermediate artefacts (like for example the output from bookmarklet-loader), since they will likely miss some functionalities (importing style, require).
The webpack output bundle is what you are looking for: a standalone JavaScript code that injects inline styles into the current page and executes your code.
2. Encode and wrap for bookmark
To convert the code into a bookmarklet, you have to encode the content for URI compatibility, and add an extra "javascript:" prefix.
This is the step where you have used the bookmarklet package. But in your case, since all you have is a single JavaScript file that you want to "hard code" into the bookmarklet, the wrapper is dead simple:
'javascript:' + encodeURIComponent('(function(){' + code + '})()')
You can continue using bookmarklet package or make it a very simple node script (but you should move the minification step in a previous step, typically in the webpack configuration).
Actually, it is quite easy to make a webpack plugin for this "bookmarkletify" step:
function AssetToBookmarkletPlugin() {}
AssetToBookmarkletPlugin.prototype.apply = function (compiler) {
compiler.plugin('emit', function (compilation, callback) {
var asset;
// Rework each asset.
for (var assetName in compilation.assets) {
asset = compilation.assets[assetName];
compilation.assets[assetName] = {
source: function () {
// Encode and wrap the original source to make it bookmark-ready.
return 'javascript:' + encodeURIComponent('(function(){' + asset.source() + '})()');
},
size: asset.size
}
}
callback();
});
};
With these additional steps (resources inlining, CSS and JS minification, bookmarkletify assets), your webpack configuration would be:
const webpack = require('webpack');
const path = require('path');
module.exports = {
entry: {
index: './src/index.js'
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
target: 'web',
module: {
rules: [{
test: /\.(png|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: {limit: 0} // 0 = always inline resource
}]
}, {
test: /\.css$/,
use: ['style-loader', {
loader: 'css-loader',
options: {minimize: true} // Minify CSS as well
}]
}]
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
new AssetToBookmarkletPlugin()
]
};
The content of dist/index.js file is now ready to be copied as a bookmark.
I guess webpack bookmarklet loader is not required to create a bookmarklet itself, as the github repo suggests
"bookmarklet-loader is a webpack loader that will convert any javascript file into a bookmarklet that can be used as a module throughout your application."
Not clear if thats your use case.
looking at the plugin code,
'use strict';
var uglify = require('uglify-js');
module.exports = function(source) {
return 'module.exports = "javascript:' + encodeURIComponent(
'(function(){'+ uglify.minify(source, { fromString: true }).code +'})();'
) + '"';
};
i suspect the issue could be because the only package used here is Uglifyjs which only compiles javascript, and no css loaders in the code.
This plugin expects your code to be pure JS and not any CSS and HTML.
From your code i see that you have configured webpack already to build css and JS, and all this code is offering you is javascript uri pattern wrapped in a function that is URI encoded.
should be pretty simple to DIY after the webpack build output.
hope that helps!

Concat and minify all less files with Webpack without importing them

I have a folder of around 20 separate less files that I need to concatenate into a single file via Webpack and store this in my /dist folder. My current Webpack config file is as follows:
const path = require('path');
const webpack = require('webpack');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const CheckerPlugin = require('awesome-typescript-loader').CheckerPlugin;
const bundleOutputDir = './wwwroot/dist';
module.exports = (env) => {
const isDevBuild = !(env && env.prod);
return [{
stats: { modules: false },
entry: { 'main': './ClientApp/boot.ts' },
resolve: { extensions: ['.js', '.ts'] },
output: {
path: path.join(__dirname, bundleOutputDir),
filename: '[name].js',
publicPath: '/dist/'
},
module: {
rules: [
{ test: /\.ts$/, include: /ClientApp/, use: 'awesome-typescript-loader?silent=true' },
{ test: /\.html$/, use: 'raw-loader' },
{ test: /\.css$/, use: isDevBuild ? ['style-loader', 'css-loader'] : ExtractTextPlugin.extract({ use: 'css-loader' }) },
{ test: /\.less/, use: ExtractTextPlugin.extract('style-loader', 'css-loader!less-loader') },
{ test: /\.(png|jpg|jpeg|gif|svg)$/, use: 'url-loader?limit=25000' }
]
},
plugins: [
new CheckerPlugin(),
new webpack.DllReferencePlugin({
context: __dirname,
manifest: require('./wwwroot/dist/vendor-manifest.json')
})
].concat(isDevBuild ? [
// Plugins that apply in development builds only
new webpack.SourceMapDevToolPlugin({
filename: '[file].map', // Remove this line if you prefer inline source maps
moduleFilenameTemplate: path.relative(bundleOutputDir, '[resourcePath]') // Point sourcemap entries to the original file locations on disk
})
] : [
// Plugins that apply in production builds only
new webpack.optimize.UglifyJsPlugin(),
new ExtractTextPlugin('site.less'),
new ExtractTextPlugin('site.css')
])
}];
};
If I try and import each single .less file into the boot.ts entry file, I get a less error stating that the less variables that I've declared are not being recognised, which is how I came to the conclusion that I need to concat these files beforehand. I come from a gulp background, so any help to get me up and running with this would be greatly appreciated.
If there is an alternative way to get all less compiled to css and working correctly, without the need for concat, then I'm open to suggestions.
Webpack is a module bundler and uses the module syntax for JavaScript (ES6, CommonJS, AMD..), CSS (#import, url) and even HTML (through src attribute) to build the app's dependency graph and then serialize it in several bundles.
In your case, when you import the *.less files the errors are because you miss CSS modules. In other words, on the places where you have used variables defined in other file, that file was not #import-ed.
With Webpack it's recommended to modularize everything, therefore I would recommend to add the missing CSS modules. I had the same issue when I was migrating a project from Grunt to Webpack. Other temporary solution is to create an index.less file where you will #import all the less files (note: the order is important) and then import that file in app's entry file (ex. boot.ts).

Content Hashes, ExtractTextPlugin and HtmlWebpackPlugin

I guess I'll start with my webpack config.
const webpack = require('webpack');
const path = require('path');
/**
* Environment
*/
const nodeEnv = process.env.NODE_ENV || 'development';
const isProduction = nodeEnv === 'production';
const isDevelopment = !isProduction;
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const sourcePath = path.join(__dirname, 'assets');
const buildPath = path.join(__dirname, 'dist');
const extractSass = new ExtractTextPlugin({
filename: '[name].[contenthash].css',
disable: isDevelopment
});
/**
* Plugins
*/
const plugins = [
new HtmlWebpackPlugin({
template: path.join(sourcePath, 'index.html'),
}),
extractSass
];
if (isProduction) {
} else {
plugins.concat([
new webpack.HotModuleReplacementPlugin(),
]);
}
module.exports = {
entry: ['./assets/app.js', './assets/app.scss'],
devtool: isProduction ? 'eval' : 'source-map',
plugins: plugins,
module: {
rules: [{
test: /\.scss$/,
use: extractSass.extract({
use: [{
loader: "css-loader"
}, {
loader: "sass-loader"
}],
// use style-loader in development
fallback: "style-loader"
})
}]
},
output: {
filename: 'bundle.js',
path: buildPath
},
devServer: {
contentBase: buildPath,
port: 9000
}
};
This all works fine when running on the webpack dev server but I'm trying to figure out how this fits together on a production environment.
As you can see, as per the sass-loader documentation, I'm creating a file called [name].[contenthash].css if NODE_ENV is set to production. I love the idea of serving files based on the content hash because I love integrity.
The difficulty I'm having is understanding how I can pass that file name, that content hash into the index.html template I'm creating so that I can <link> the stylesheet.
Is it a server side thing?
Is there any way to pass that file name into the HTML template on
production?
Is it intentional that I do it manually or script it out?
I just don't understand how these two components come together to produce a publishable build. HtmlWebpackPlugin produced a .html in the output directory but obviously it has no innate understanding of where to find it's styles.
Your config seems correct.
Is there any way to pass that file name into the HTML template on production?
What should be happening is that the HtmlWebpackPlugin should be creating a new index.html file in your buildPath directory, which has the generated bundles automatically injected in it (for example the generated CSS bundle will be injected in the head tag and the generated script bundles at the bottom of the body tag)
Beyond that it is just a matter of serving that dist/index.html to whoever visits your site/app. So the answer to
Is it a server side thing?
is yes.
Try doing a build without the dev server, by simply running webpack, so you can see the output of your configuration (the dev server builds things in memory, so you do not actually get to see them)

Categories

Resources