three.js ES6 how import only specific modules - javascript

I've installed three.js library through NPM to get advantage of the new ES6 modular architecture which should let you to import just the modules you need, as explained here: Threejs - Import via modules.
I am using gulp, browserify and babel for bundling and transpiling, like so:
gulp.task("build_js", () => {
return browserify({
entries: "./public/app/app.js",
cache: {},
dev: true
})
.transform(babelify, {presets: ["env"], plugins: ["syntax-async-functions", "transform-async-to-generator"], sourceMaps: true})
.bundle()
.pipe(source("app.bundle.min.js"))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: mode}))
.pipe(uglify())
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest(config.build.js))
});
I want to import only the modules I need and keep the bundle size small, but I noticed that the bundle generated by browserify has the same size regardless if I import all the modules or just one.
If in app.js I import all the modules I got a bundle size of about 500Kb:
// app.js
import * as THREE from 'three'; // about 500 Kb size
But if I try to import just a specific module using ES6 syntax I got the same bundle size (it is importing again all the modules):
// app.js
import { Vector3 } from 'three'; // about 500 Kb size, same as before example
I've also tried the following:
// app.js
import { Vector3 } from "three/build/three.module.js";
But I got the following error:
SyntaxError: 'import' and 'export' may only appear at the top level (45590:0) while parsing /Users/revy/proj001/node_modules/three/build/three.module.js
My question: how can I properly import only the modules I need and keep the bundle size small?

You are missing the concept of Tree Shaking.
When you import a modules by name the other modules are not automatically removed from the bundle. The bundler always includes every module in the code and ignores what you have specified as import names.
The other unused modules, which you did not import, are considered dead code because they are in the bundle however they are not called by your code.
So to remove this unused code from the bundle and thus make the bundle smaller you need a minifier that supports dead code removal.
Check out this popular tree shaking plugin for browserify - it should get you started:
https://github.com/browserify/common-shakeify

Solved using rollupify inside browserify transform. It will perform tree shaking and remove dead code:
gulp.task("build_js", () => {
return browserify({
entries: "./public/app/app.js",
cache: {},
dev: true
})
.transform(rollupify, {config: {}}) // <---
.transform(babelify, {presets: ["env"], plugins: ["syntax-async-functions", "transform-async-to-generator"], sourceMaps: true})
.bundle()
.pipe(source("app.bundle.min.js"))
.pipe(buffer())
.pipe(sourcemaps.init({loadMaps: mode}))
.pipe(uglify())
.pipe(sourcemaps.write("./"))
.pipe(gulp.dest(config.build.js))
});
}
Still I would appreciated an explanation on why ES6 module import works like this..

Related

Tailor dependencies to ES or CJS with Rollup

I have a NPM package (private) which works in both a browser and Node environment.
This is done by creating separate bundles via Rollup for ES and CJS, so the output looks like:
dist/ejs/index.js // Import this for your browswer environments
dist/cjs/index.js // Use this for Node environments
Pretty standard. Now I'm adding a dependency to this, which follows the same bundling pattern.
I can import the library like so:
import { externalLibrary } from "#external/ejs/externalLibrary";
All is good in a browser environment. But now this does not work in a Node environment, as what I'm importing is not CJS.
I could change the way I import the library to require and target the cjs bundle:
const { externalLibrary } = require("#external/cjs/externalLibrary");
And while this works in both environments, I don't think it's optimal.
Is there a better way of doing this? Some configuration that I could specify when exporting the CJS bundle?
module.exports = {
input: 'src/main.js',
output: {
file: 'bundle.js',
format: 'cjs'
// Behaviour here for #external/cjs/externalLibrary ?
}
};
I overlooked the package.json config. You can specify different entry files depending on the build here:
{
...
"main": "dist/cjs/index.js",
"module": "dist/ejs/index.js",
...
}
Then I removed the implicit import of the EJS file, and targeted just the package:
// Before:
import { externalLibrary } from "#external/dist/ejs/externalLibrary";
// After:
import { externalLibrary } from "#external";
This then ensures either the CJS or ES build is used, depending on the environment using the package.
Looks like you already found the solution for this, but even with old import style
import { externalLibrary } from "#external/dist/ejs/externalLibrary";
you should be able to target appropriate formats for cjs vs esm. With rollup, you would have to configure the output config to be an array of objects with appropriate format set. For example:
module.exports = {
input: 'src/main.js',
output: [{ file: 'dist/index.cjs.js', format: 'cjs' },
{ file: 'dist/index.esm.js', format: 'es' }],
}
Also, being an author of klap, I would recommend giving it a try as it would bring in lot of other optimizations by default.

Tree-shaking with rollup

I have a project in which I bundle a components library using Rollup (generating a bundle.esm.js file). These components are then used in another project, that generates web pages which use these components - each page is using different components.
The problem is, that the entire components library is always bundled with the different page bundles, regardless of which components I'm using, unnecessarily increasing the bundle size.
This is my Rollup setup:
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import pkg from './package.json';
const extensions = [
'.js', '.jsx', '.ts', '.tsx',
];
export default [
{
input: './src/base/index.ts',
plugins: [
peerDepsExternal(),
resolve({ extensions }),
babel({
exclude: 'node_modules/**',
extensions,
}),
commonjs(),
],
output: [
{ file: pkg.main, format: 'cjs', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true },
],
watch: {
clearScreen: false,
},
},
];
I have "modules" set to false in webpack, as well.
There are things you will need to do to achieve treeshakable code from both sides - the built package and the project using it.
From your code snippet, I see that you have not add flag preserveModules: true in the rollup config file to prevent the build output from bundling. Webpack can not treeshake a bundled file FYI.
export default {
...
preserveModules: true,
...
}
On the side of the project that using it, you have to specify sideEffects in the package.json - read the doc to know how to config them. Beside that, the optimization in webpack has to has sideEffects: true, also read the doc here.
Hope this helps!
As you don't know which components of your Component Library (CL) will be needed by the adopters repositories you need to export everything but in a way
the adopters can execute a tree-shaking on your CL when they do their own build (and just include what they really need).
In a few words, you have to make your CL, tree-shakable. In order to achieve this, on your CL repo you have to:
Use bundlers that support tree-shaking (rollup, webpack, etc..)
Create the build for modules of type es/esm, NOT commonJS/cjs, etc..
Ensure no transpilers/compilers (babel,tsconfig, etc..) usually used as plugins, transform your ES module syntax to another module syntax.
By the default, the behavior of the popular Babel preset #babel/preset-env may break this rule, see the documentation for more details.
// babelrc.json example that worked for me
[
"#babel/preset-env",
{
"targets": ">0.2%, not dead, not op_mini all"
}
],
In the codebase, you always have to use import/export (no require) syntax, and import specifically the things you need only.
import arrayUtils from "array-utils"; //WRONG
import { unique, implode, explode } from "array-utils"; //OK
Configure your sideEffects on the package.json.
"sideEffects": ["**/*.css"], //example 1
"sideEffects": false, //example 2
DO NOT create a single-bundle file but keep the files separated after your build process (official docs don't say this but was the only solution that worked for me)
// rollup.config.js example
const config = [
{
input: 'src/index.ts',
output: [
{
format: 'esm', // set ES modules
dir: 'lib', // indicate not create a single-file
preserveModules: true, // indicate not create a single-file
preserveModulesRoot: 'src', // optional but useful to create a more plain folder structure
sourcemap: true, //optional
},
],
... }]
Additionally, you may need to change your module entry point in order the adopters can directly access to the proper index.js file where you are exporting everthing:
// package.json example
{
...
"module": "lib/index.js", //set the entrypoint file
}
Note: Remember that tree-shaking is executed by an adopter repository that has a build process that supports tree-shaking (eg: a CRA repo) and usually tree-shaking is just executed on prod mode (npm run build), no on dev mode. So be sure to properly test if this is working or not.

Three.js specific imports failing, possibly Webpack's fault

Disclaimer: I'm using create-react-app and three.js v0.99.0.
I'm trying to import specific three.js modules, since importing straight from the root module includes the entire library in the bundle, which is 0.5MB uncompressed. Most direct src/ imports work fine, however when importing Geometry, AKA changing this:
import { Geometry } from "three";
to this:
import { Geometry } from "three/src/core/Geometry";
The line on my graph that it was drawing no longer appears, and there's no error messages. In addition, importing the WebGLRendered straight from src/ caused the whole thing to implode:
import { WebGLRenderer } from "three"; // from this
import { WebGLRenderer } from "three/src/renderers/WebGLRenderer"; // to this
with the error:
WebGLIndexedBufferRenderer.js:14 Uncaught TypeError: Cannot read property 'type' of undefined
at WebGLIndexedBufferRenderer.setIndex (WebGLIndexedBufferRenderer.js:14)
at WebGLRenderer.renderBufferDirect (WebGLRenderer.js:505)
at renderObject (WebGLRenderer.js:932)
at renderObjects (WebGLRenderer.js:913)
at WebGLRenderer.render (WebGLRenderer.js:790)
at SceneManager.js:75
...
I checked the definitions of these modules in the three.js library, and they are literally copy pasted between three.module.js and src/, so there's no code differences I can find. However, one thing I did notice is that if I import both and print them, Webpack seems to be transpiling the one I import from src/:
ƒ Geometry() {
Object.defineProperty(this, 'id', {
value: geometryId += 2
});
this.uuid = _math_Math_js__WEBPACK_IMPORTED_MODULE_10__["_Math"].generateUUID();
...
as opposed to:
ƒ Geometry() {
Object.defineProperty(this, 'id', {
value: geometryId += 2
});
this.uuid = _Math.generateUUID();
...
is it possible that create-react-app's Webpack is messing with imports that are within a src/ folder, thus making them not work despite the source code being identical? If so, is there a way to make it only transpile code within MY src/ folder, and not the 3rd partiy modules'?
It's probably a babel issue. By default, babel won't transpile anything in node_modules, however you're importing from the ES6 source, so you need to force it to transpile these. In your webpack config, you should have a rule (in module.rules) for js(x) files that looks something like
{
test: /\.jsx?$/,
use: 'babel-loader'
}
add a custom exclude that will exclude everything in node_modules except the three files, i.e.
{
test: /\.jsx?$/,
exclude: /node_modules\/(?!three)/,
use: 'babel-loader'
}

Webpack multiple files entry bundle

Will webpack produce different results given a configuration like so:
// webpack.config.js
module.exports = {
...
entry: {
main: ['./index.js'],
}
}
// index.js
import 'babel-polyfill'
...
vs.
// webpack.config.js
module.exports = {
...
entry: {
main: ['babel-polyfill', './index.js'],
}
}
// index.js
// babel-polyfill import removed
...
Which one is preferred, and why?
Both works kind of the same way.
The option 1, webpack would treat babel-polyfill as a dependency, in the dependency tree.
The second one, webpack would treat babel-polyfill as an entrypoint, where it would try to generate a dependency graph from that, which would have 0 dependencies.
There is no real difference here, nor any impact on the result bundle, both will contain babel-polyfill anyways, and also there is no "preferred" way to add that, babel itself refeers to both ways on their guide.
The result will be the same. It depends on you, usually I prefer to import the dependencies in modules rather than importing implicitly in webpack config.

Gulp and Babel: Error: Cannot find module

I have a project that I've set up using gulp and babel. Everything is working fine, except when I create a module and import it once it's converted from ES6 to ES6 it doesn't work. I get an error:
Error: Cannot find module 'hello.js'
at Function.Module._resolveFilename (module.js:440:15)
at Function.Module._load (module.js:388:25)
at Module.require (module.js:468:17)
Here's my gulpfile.babel.js:
import gulp from "gulp";
import babel from "gulp-babel"
import concat from "gulp-concat"
const dirs = {
src: "src",
dest: "build"
}
gulp.task("build", () => {
return gulp.src(dirs.src + "/**/*.js")
.pipe(babel())
.pipe(concat("build.js"))
.pipe(gulp.dest(dirs.dest))
});
gulp.task("default", ["build"]);
During build everything is concatenated into one file. Under src/ I have:
app.js
hellojs
app.js
import hello from './hello.js'
console.log(hello());
hello.js
export default () => {
return 'Hey from hello.js';
};
And I run like so:
npm start
Which basically calls node ./build/build.js.
I think it's because it's concatenating the ES6 into ES5 and the bundle.js still contains the require for hello.js. It wont find it though because its concatenated. Is that possible?
It is incorrect to concatenate two module files and expect the program to work properly, even when transpiled to ES5. Bundling involves more than concatenating the scripts: each module needs a closure for registering exports and resolving the contents of other modules.
You must instead use a bundling tool such as Browserify, Webpack or Rollup. Here's how one would bundle with Browserify (which in this case, it is easier to rely on the Babelify transform rather than gulp-babel):
var browserify = require('browserify');
var gulp = require('gulp');
var source = require('vinyl-source-stream');
var babelify = require('babelify');
gulp.task('browserify', function() {
return browserify({
entries: './src/app.js'
})
.transform(babelify)
.bundle()
.pipe(source('bundle.js'))
.pipe(gulp.dest('./build/'));
});

Categories