Migrating legacy library to ES2015-style modules - javascript

I have a javascript library intented to work in the browser, that I package with the following kind of gulpfile:
gulp.task("build", function() {
return gulp.src(sourceFiles)
.pipe(sourcemaps.init())
.pipe(concat("lib.js"))
.pipe(sourcemaps.write())
.pipe(gulp.dest("dist/"));
});
I want to start migrating this library to ES2015, using Babel.
For now, each source file in the src/ folder represents a module and is written using the following convention.
In src/MyModule.js:
MyLib.MyModule = (function () {
var module = {};
// code here...
return module;
})();
I want to migrate these scripts to ES2015-style modules, but I still want my releases to contain a single script (here lib.js). The consumers of my library would then load my modules using AMD implementations (e.g. require.js).
Is it possible to achieve such a thing? How would I do this?
EDIT:
I don't need my modules to remain nested like they currently are (Foo.Bar.Baz). But I do need my modules to be compatible with Flow.

As suggested by #JoeClay, I ended up packing my code using Webpack. I am setting it up to output in the UMD format, for better portability. Here is a summary of my gulpfile.js:
var gulp = require('gulp');
var sourcemaps = require('gulp-sourcemaps');
var webpack = require('webpack');
var webpackStream = require('webpack-stream');
var babel_loader = {
test: /\.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
query: {
presets: babel_presets,
plugins: babel_plugins
}
};
var webpackConfig = {
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-app.js',
library: 'MyApp',
libraryTarget: 'umd'
},
module: {
loaders: [
babel_loader
]
},
externals: externals,
devtool: "source-map"
};
gulp.task('build', function () {
return gulp.src("src/index.js")
.pipe(webpackStream(webpackConfig, webpack))
.pipe(sourcemaps.init({loadMaps: true}))
// some additional transformations here (e.g. uglify)...
.pipe(sourcemaps.write())
.pipe(gulp.dest('./dist/'));
});
Notes on source files
You don't need to list all of your source files. Webpack resolves the dependencies by itself by scanning recursively the sources, starting from the specified entry points (here just “src/index.js”). This approach required me to adapt my legacy code to using ES2015-style module import/export.
Notes on source maps
Webpack can generate source maps in a variety of flavours (here I use source-map, which is the best quality but takes more time to generate). If you want to do additional transformations to your code, you just have to use the gulp-sourcemaps plugin the usual way, so that the original source maps are adapted automatically. You need to initialize gulp-sourcemaps after the Webpack pass, using the loadMaps option.

Related

Public file is missing from build/public in Webpack [duplicate]

I'm trying to move from Gulp to Webpack. In Gulp I have task which copies all files and folders from /static/ folder to /build/ folder. How to do the same with Webpack? Do I need some plugin?
Requiring assets using the file-loader module is the way webpack is intended to be used (source). However, if you need greater flexibility or want a cleaner interface, you can also copy static files directly using my copy-webpack-plugin (npm, Github). For your static to build example:
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
context: path.join(__dirname, 'your-app'),
plugins: [
new CopyWebpackPlugin({
patterns: [
{ from: 'static' }
]
})
]
};
Compatibility note: If you're using an old version of webpack like webpack#4.x.x, use copy-webpack-plugin#6.x.x. Otherwise use latest.
You don't need to copy things around, webpack works different than gulp. Webpack is a module bundler and everything you reference in your files will be included. You just need to specify a loader for that.
So if you write:
var myImage = require("./static/myImage.jpg");
Webpack will first try to parse the referenced file as JavaScript (because that's the default). Of course, that will fail. That's why you need to specify a loader for that file type. The file- or url-loader for instance take the referenced file, put it into webpack's output folder (which should be build in your case) and return the hashed url for that file.
var myImage = require("./static/myImage.jpg");
console.log(myImage); // '/build/12as7f9asfasgasg.jpg'
Usually loaders are applied via the webpack config:
// webpack.config.js
module.exports = {
...
module: {
loaders: [
{ test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/, loader: "file" }
]
}
};
Of course you need to install the file-loader first to make this work.
If you want to copy your static files you can use the file-loader in this way :
for html files :
in webpack.config.js :
module.exports = {
...
module: {
loaders: [
{ test: /\.(html)$/,
loader: "file?name=[path][name].[ext]&context=./app/static"
}
]
}
};
in your js file :
require.context("./static/", true, /^\.\/.*\.html/);
./static/ is relative to where your js file is.
You can do the same with images or whatever.
The context is a powerful method to explore !!
One advantage that the aforementioned copy-webpack-plugin brings that hasn't been explained before is that all the other methods mentioned here still bundle the resources into your bundle files (and require you to "require" or "import" them somewhere). If I just want to move some images around or some template partials, I don't want to clutter up my javascript bundle file with useless references to them, I just want the files emitted in the right place. I haven't found any other way to do this in webpack. Admittedly it's not what webpack originally was designed for, but it's definitely a current use case.
(#BreakDS I hope this answers your question - it's only a benefit if you want it)
Webpack 5 adds Asset Modules which are essentially replacements for common file loaders. I've copied a relevant portion of the documentation below:
asset/resource emits a separate file and exports the URL. Previously achievable by using file-loader.
asset/inline exports a data URI of the asset. Previously achievable by using url-loader.
asset/source exports the source code of the asset. Previously achievable by using raw-loader.
asset automatically chooses between exporting a data URI and emitting a separate file. Previously achievable by using url-loader with asset size limit.
To add one in you can make your config look like so:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/,
type: "asset/resource"
}
]
}
};
To control how the files get output, you can use templated paths.
In the config you can set the global template here:
// webpack.config.js
module.exports = {
...
output: {
...
assetModuleFilename: '[path][name].[hash][ext][query]'
}
}
To override for a specific set of assets, you can do this:
// webpack.config.js
module.exports = {
...
module: {
rules: [
{
test: /\.(jpe?g|gif|png|svg|woff|ttf|wav|mp3)$/,
type: "asset/resource"
generator: {
filename: '[path][name].[hash][ext][query]'
}
}
]
}
};
The provided templating will result in filenames that look like build/images/img.151cfcfa1bd74779aadb.png. The hash can be useful for cache busting etc. You should modify to your needs.
Above suggestions are good. But to try to answer your question directly I'd suggest using cpy-cli in a script defined in your package.json.
This example expects node to somewhere on your path. Install cpy-cli as a development dependency:
npm install --save-dev cpy-cli
Then create a couple of nodejs files. One to do the copy and the other to display a checkmark and message.
copy.js
#!/usr/bin/env node
var shelljs = require('shelljs');
var addCheckMark = require('./helpers/checkmark');
var path = require('path');
var cpy = path.join(__dirname, '../node_modules/cpy-cli/cli.js');
shelljs.exec(cpy + ' /static/* /build/', addCheckMark.bind(null, callback));
function callback() {
process.stdout.write(' Copied /static/* to the /build/ directory\n\n');
}
checkmark.js
var chalk = require('chalk');
/**
* Adds mark check symbol
*/
function addCheckMark(callback) {
process.stdout.write(chalk.green(' ✓'));
callback();
}
module.exports = addCheckMark;
Add the script in package.json. Assuming scripts are in <project-root>/scripts/
...
"scripts": {
"copy": "node scripts/copy.js",
...
To run the sript:
npm run copy
The way I load static images and fonts:
module: {
rules: [
....
{
test: /\.(jpe?g|png|gif|svg)$/i,
/* Exclude fonts while working with images, e.g. .svg can be both image or font. */
exclude: path.resolve(__dirname, '../src/assets/fonts'),
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'images/'
}
}]
},
{
test: /\.(woff(2)?|ttf|eot|svg|otf)(\?v=\d+\.\d+\.\d+)?$/,
/* Exclude images while working with fonts, e.g. .svg can be both image or font. */
exclude: path.resolve(__dirname, '../src/assets/images'),
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/'
},
}
]
}
Don't forget to install file-loader to have that working.
You can write bash in your package.json:
# package.json
{
"name": ...,
"version": ...,
"scripts": {
"build": "NODE_ENV=production npm run webpack && cp -v <this> <that> && echo ok",
...
}
}
Most likely you should use CopyWebpackPlugin which was mentioned in kevlened answer. Alternativly for some kind of files like .html or .json you can also use raw-loader or json-loader. Install it via npm install -D raw-loader and then what you only need to do is to add another loader to our webpack.config.js file.
Like:
{
test: /\.html/,
loader: 'raw'
}
Note: Restart the webpack-dev-server for any config changes to take effect.
And now you can require html files using relative paths, this makes it much easier to move folders around.
template: require('./nav.html')
I was stuck here too. copy-webpack-plugin worked for me.
However, 'copy-webpack-plugin' was not necessary in my case (i learned later).
webpack ignores root paths
example
<img src="/images/logo.png'>
Hence, to make this work without using 'copy-webpack-plugin'
use '~' in paths
<img src="~images/logo.png'>
'~' tells webpack to consider 'images' as a module
note:
you might have to add the parent directory of images directory in
resolve: {
modules: [
'parent-directory of images',
'node_modules'
]
}
Visit https://vuejs-templates.github.io/webpack/static.html
The webpack config file (in webpack 2) allows you to export a promise chain, so long as the last step returns a webpack config object. See promise configuration docs. From there:
webpack now supports returning a Promise from the configuration file. This allows to do async processing in you configuration file.
You could create a simple recursive copy function that copies your file, and only after that triggers webpack. E.g.:
module.exports = function(){
return copyTheFiles( inpath, outpath).then( result => {
return { entry: "..." } // Etc etc
} )
}
lets say all your static assets are in a folder "static" at the root level and you want copy them to the build folder maintaining the structure of subfolder, then
in your entry file) just put
//index.js or index.jsx
require.context("!!file?name=[path][name].[ext]&context=./static!../static/", true, /^\.\/.*\.*/);
In my case I used webpack for a wordpress plugin to compress js files, where the plugin files are already compressed and need to skip from the process.
optimization: {
minimize: false,
},
externals: {
"jquery": "jQuery",
},
entry: glob.sync('./js/plugin/**.js').reduce(function (obj, el) {
obj[path.parse(el).name] = el;
return obj
}, {}),
output: {
path: path.resolve(__dirname, './js/dist/plugin'),
filename: "[name].js",
clean: true,
},
That used to copy the js file as it is to the build folder. Using any other methods like file-loader and copy-webpack create issues with that.
Hope it will help someone.

bundle web workers as integral part of npm package with single file webpack output

I am writing an npm package which is a plugin for the popular library leafletjs. I am using webpack to bundle the package. I want this package to be able to spawn and destroy some web workers on command. The web worker code is part of my source files. But I want to be able to distribute my package both as an npm module, or through a cdn, meaning it must be compiled down to a singular file that can be included through an HTML header. I am using webpack to do it. So lets say I have a worker file:
// sample.worker.js
import { doSomeStuff } from './algorithm.js';
onmessage = function (e) {
const results = doSomeStuff(e.data)
postMessage({
id: e.data.id,
message: results',
});
};
Fairly simple, but an important point here is that my worker is actually importing some code from an algorithm file, which is in turn importing some node modules. My worker is used in the main module somewhere, like this:
// plugin.js
import SampleWorker from 'worker-loader!./workers/dem.worker.js';
export function plugin(){
// do some stuff
const worker = new SampleWorker()
worker.postMessage(someData);
worker.onmessage = (event) => {};
}
My webpack config looks like this:
module.exports = {
entry: './src/index',
output: {
path: path.resolve(__dirname, 'build'),
filename: 'my-plugin.js',
},
resolve: {
extensions: ['.ts', '.tsx', '.js', '.json'],
},
module: {
rules: [
{
test: /\.worker\.js$/,
loader: 'worker-loader',
options: {
inline: 'fallback',
},
},
{
test: /\.(ts|js)x?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
],
},
externals: { ... },
plugins: { ... }
};
This doesn't work as is - webpack tries to bundle the main bundle and each worker script file under the same name. Changing to filename: '[name].js' under output fixes this issue, but gives me many files - one for the main bundle, and another file for each worker file in my source code.
Reading the webpack options, I thought that using the inline: 'fallback' option would actually create a Blob for each worker and bundle that into the main output file. That is not happening.
So far my solution is to write my workers as blobs, like this:
// workers.js
const workerblob = new Blob([`
// cannot import in a blob!
// have to put algorithm.js code here, copy in any external module code here
onmessage = function (e) {
const results = doSomeStuff(e.data)
postMessage({
id: e.data.id,
message: results,
});
};
`])
export const sampleWorker = URL.createObjectURL(workerblob);
// my-plugin.js
import { sampleWorker } from 'workers.js'
const worker = new Worker(sampleWorker)
This does in fact work - webpack now outputs 1 single file which includes the worker code. Using a modified version of this from this answer, I can at least place my code inside a function( ...code... ){}.toString() format, so I can at least get intellisense, syntax highlighting, etc. But I cannot use imports.
How can I use webpack to bundle my workers so that the entire bundle ends up in 1 file, worker code and all?

Webpack authoring libraries import errors with TypeError: xxx is not a constructor

I am having trouble importing a javascript package I wrote (and published to npm) into a new project.
I created an ES6 package and bundled it using webpack. Within the package I can import the library using the bundled file using a script tag,
<script src='../dist/awesome-table-dist.js'></script>
then new up the class like so:
let awesomeTable = new AwesomeTable.AwesomeTable('record');
Works like a charm! awesome-table
I pushed the package up to npm, now I want to bring it into a new project like so:
import AwesomeTable from '#iannazzi/awesome-table'
then
let awesomeTable = new AwesomeTable('record');
which is erroring:
TypeError: _iannazzi_awesome_table__WEBPACK_IMPORTED_MODULE_0___default.a is not a constructor
Now that I am importing it to a new project I have tried a variety of ways new up the class, but seem completely stuck.
I can use the package by including the script again, but obviously I want to import so I can re-package:
<script src='node_modules/#iannazzi/awesome-table/dist/awesome-table-dist.js'></script>
Here is the webpack configuration for the package:
var path = require('path');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const SRC_DIR = path.resolve(__dirname,'../src');
const DIST_DIR = path.resolve(__dirname, '../dist');
module.exports = {
entry: {
app: SRC_DIR + '/table/AwesomeTable.js'
},
output: {
path: DIST_DIR,
filename: 'awesome-table-dist.js',
library: 'AwesomeTable',
// libraryTarget: 'window'
},
module: {
rules: [
{
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
],
},
plugins: [
new ExtractTextPlugin('style.css')
//if you want to pass in options, you can do so:
//new ExtractTextPlugin({
// filename: 'style.css'
//})
]
};
First of all, you need to expose your lib, you can do that via defining library & libraryTarget: 'umd' properties.
Second, it is not recommended to publish lib as a bundle. Think about that scenario, You lib is composed from several parts, but not all of them are mandatory. When you ship your lib a a bundle, you are forcing your users to download redundant extra code.
The best practice today is to transpile your code via Babel to be compatible es5 and commonjs as a module system.
Recently, there is a trend to ship es6 modules in a separate folder, so that whenever your users will use bundles that supports tree-shaking, they will able to use it.

Are entry points required for non JavaScript assets in web pack 4?

The only way I have successfully had webpack generate a non JavaScript file is to include an entry for the primary asset. The problem is, webpack is generating a .js file based on this asset as well, which is unnecessary. Is this the correct way of working with non JavaScript assets in a webpack config?
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const outputDir = 'build';
const extractStylus = new ExtractTextPlugin('../css/screen.css');
module.exports = {
entry: {
app: './src/js/index.js',
print: './src/js/print.js',
stylus: './src/stylus/screen.styl'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
},
{
test: /\.styl$/,
use: extractStylus.extract({
fallback: 'style-loader',
use: ['css-loader', 'stylus-loader']
})
}
]
},
plugins: [extractStylus],
output: {
filename: '[name].bundle.js',
path: path.resolve(__dirname, `${outputDir}/js`)
}
};
The specific line in question is part of the entry object:
stylus: './src/stylus/screen.styl'
Without that line, nothing is generated, but with that line, the expected .css as well as a stylus.bundle.js file are generated.
I think you misunderstand what the entry property does in a webpack config:
An entry point indicates which module webpack should use to begin building out its internal dependency graph. After entering the entry point, webpack will figure out which other modules and libraries that entry point depends on (directly and indirectly).
Every dependency is then processed and outputted into files called bundles, which we'll discuss more in the next section.
[source, emphasis mine]
Without specifying an entry, webpack would not know where to look for your files; even if the dependency graph was not directional (which it is), you need to point webpack to at least one point of the graph.
The minor problem of having a JS file generated even though you are only processing assets is a consequence of how webpack generally is used – as an asset manager / compiler for some application logic written in JS. So, in theory, if you needed to use the compiled assets via NodeJS style requires, you'd use the generated stylus.bundle.js.

Pass an argument to Webpack from Node.js

I'm trying to build both minified and unminified versions of my app (js and css) using Webpack.
This can be easily done via command-line interface using -p or --optimize-minimize switch:
webpack
webpack -p
However, I would like to perform these actions with just one command, so I decided to write a small Node.js script which would run both of these Webpack builds:
var webpack = require('webpack');
var config = require('./webpack.config');
webpack(config, callback); // Build unminified version
So the question is: can I pass the aforementioned -p argument to Webpack from the Node.js script in order to build the minified version? Or maybe there is a simpler way of solving my particular problem?
Of course, I can use child_process.exec(), but I don't think it's an authentic Node.js way.
Create your default config to webpack an unminified version. Run webpack with that config. Change the configuration from code and run webpack again. Here is a code example.
var webpack = require('webpack');
//assuming the config looks like this.
var config = {
entry: "./entry.js",
output: {
devtoolLineToLine: true,
sourceMapFilename: "./bundle.js.map",
pathinfo: true,
path: __dirname,
filename: "bundle.js"
},
module: {
loaders: [
{ test: /\.css$/, loader: "style!css" }
]
},
plugins: []
};
webpack(config).run(); // Build unminified version
config.output.filename = 'bundle.min.js'
config.plugins = [
new webpack.optimize.UglifyJsPlugin({
include: /\.min\.js$/,
minimize: true
})];
webpack(config).run(); // Build minified version
Key p is an alias to setting node environment variable process.env.NODE_ENV="production" as described here

Categories

Resources