ES6 Dynamic Imports using Webpack and Babel - javascript

I've been using Webpack for my ES6 JS project and has been going well until I started to play with dynamic imports.
What I had that worked (router.js):
import { navigo } from "Navigo"; // router
import { clients } from "Controllers/clients.js";
const navigo = new Navigo();
navigo_router.on({
'/clients': () => {
clients.init();
}
});
But the more pages/routes I add, the more imports get stacked up in the head of the module. This is a relatively large app and I have a lot of pages/routes to add and therefore I need to load them dynamically to reduce the size of the initial page load.
So, following Webpack's documentation for dynamic imports, I tried the following which loads the controller module only when the relative route is called:
import { navigo } from "Navigo"; // router
const navigo = new Navigo();
navigo_router.on({
'/clients': () => {
import("Controllers/clients.js").then((clients) => {
clients.init();
});
}
});
But saving this in my editor resulted in a Babel transpiling error; SyntaxError: 'import' and 'export' may only appear at the top level, and clients.init() is not being called when tested in browser.
After a bit of reading, I discovered I needed a Babel plugin to transpile dynamic import() to require.ensure. So, I installed the plugin using the following command:
npm install babel-plugin-dynamic-import-webpack --save-dev
And declared the plugin in my babel.rc file
{ "plugins": ["dynamic-import-webpack"] }
After installing the plugin, the transpiling error disappeared and checking my transpiled code I found that the dynamic import()s has in fact been changed to require.ensure as expected. But now I get the following browser errors when testing:
Error: Loading chunk 0 failed.
Stack trace:
u#https://<mydomain.com>/js/app.bundle.js:1:871
SyntaxError: expected expression, got '<' 0.app.bundle.js:1
Error: Loading chunk 0 failed.
I didn't understand why it was referencing 0.app.bundle.js with the 0. prefix, so I checked my output/dist folder and I now have a new file in there called 0.app.bundle.js:
0.app.bundle.js 1,962bytes
app.bundle.js 110,656bytes
I imagine this new bundled file is the dynamically imported module, clients.js.
I only added dynamic importing to that one route and have left all the other routes as they were. So, during testing, I can view all routes except that one /clients route that now throws the above errors.
I'm totally lost at this point and hoped somebody could help push me over the finish line. What is this new file 0.app.bundle.js and how am I supposed to be using it/including it in my application?
I hope I've explained myself clearly enough and look forward to any responses.

I managed to fix my own problem in the end, so I will share what I discovered in an answer.
The reason the chunk file wasn't loading was because Webpack was looking in the wrong directory for it. I noticed in the Network tab of my developer console that the the chunk file/module was being called from my root directory / and not in /js directory where it belongs.
As per Webpack's documentation, I added the following to my Webpack config file:
output: {
path: path.resolve(__dirname, 'dist/js'),
publicPath: "/js/", //<---------------- added this
filename: 'app.bundle.js'
},
From what I understand, path is for Webpack's static modules and publicPath is for dynamic modules.
This made the chunk load correctly but I also had further issues to deal with, as client.init() wasn't being called and yielded the following error:
TypeError: e.init is not a function
To fix this, I also had to change:
import("Controllers/clients.js").then((clients) => {
clients.init();
});
To:
import("Controllers/clients.js").then(({clients}) => {
clients.init();
});
Note the curly braces in the arrow function parameter.
I hope this helps somebody else.

For debugging, you need to do
import("Controllers/clients.js").then((clients) => {
console.log(clients);
});
maybe working
import("Controllers/clients.js").then((clients) => {
clients.default.init();
});

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.

How do I Resolve an Alias with Node Sass in a .mjs Script?

My project's structure has a SCSS library for global settings and such, and main component scss modules are elsewhere with their corresponding component files. I have my Webpack set up to resolve a path alias # when it builds, so my usage of lines like #import "#/src/scss/variables/blah-blah.scss" works out just fine when running the project.
Conversely, I have some utility scripts i'm using to do SCSS benchmarking, just some compile and analyze operations to help illustrate our performance gains while we work on style changes. I have these built using the .mjs file extension, which I don't think is important here, it's just helping play nice with Typescript.
So when Node Sass encounters a path like #import "#/src/scss/variables/blah-blah.scss" it won't resolve the path. No surprise there, but I can't seem to locate a best solution to just help resolve the path within my script, rather than add on NPM packages to do a seemingly simple job. How should I be approaching this?
Error comes up from await sass.render()
import { writeFileSync } from "fs";
import sass from "node-sass";
import tildeImporter from "node-sass-tilde-importer";
import { resolve } from "path";
export const compileCSS = async module => {
const input = `src/components/${module}/scss/module.scss`;
const output = `stats/css/${module}.css`;
await sass.render(
{
file: resolve(input),
importer: tildeImporter,
outputStyle: "expanded",
outFile: resolve(output),
sourceMap: true
},
async function(error, result) {
if (error) {
await console.log(module, "COMPILE ERROR", error.message);
} else {
writeFileSync(resolve(output), result);
return result;
}
}
);
};
Further research shows me that Node Sass does not currently have a great way to enable this, though some ideas appear in the works.
I made a work around by converting my stats gathering operations into a Webpack loader module that analyzes and spits out my data in between steps to pack the compiled styles into the bundle. This way I get the alias path resolved and simplify the dev experience. It's a better solution than I had set out for so I'm pleased.

TS2307: error module not found local geojson in Angular

I want to draw a world map using d3-geo-projection. As input file I've thus far used https://enjalot.github.io/wwsd/data/world/world-110m.geojson, but I want to switch to a locally hosted version of that exact file.
Code I'm using that works fine:
const url = "https://enjalot.github.io/wwsd/data/world/world-110m.geojson";
d3.json(url).then(function(geojson) {
svg.append("path").attr("d", path(geojson))
});
Using examples and tutorials, I've added import world110m from './world110m.geojson'; with world110m.geojson in the same folder as the file, and changed const url"..."; to const url = world110m;.
This results in 2 errors in Terminal:
ERROR in ./src/app/02_robinson_projection/world110m.geojson 1:7
Module parse failed: Unexpected token (1:7)
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
{"type":"Fea...
and
ERROR in src/app/02_robinson_projection/robinson-projection.component.ts(5,23): error TS2307: Cannot find module './world110m.geojson'.
Some googling resulted in having to add:
"compilerOptions": {
"resolveJsonModule": true,
"esModuleInterop": true,
...
}
However, the errors stay and the page in the browser gets a black overlay giving the first error.
I've looked in integrating webpack further in my project (https://webpack.js.org/concepts/loaders/), but when I look for a geojson-loader all I find is the old json-loader, which I've included instead (even though it says it's not necessary any more).
Together with this I also created a webpack.config.js,
module.exports = {
module: {
rules: [
{ test: /\.geojson$/, use: 'json-loader' }
]
}
};
But I'm quite sure this code is incorrect.
I know it's comparing apples to oranges, but why is including a local (geo)json file in Angular such a hassle? When I try above in plain JS with a local geojson it works just fine.

Resolving dynamic module with Webpack and Laravel Mix

Good time of the day,
Recently I've been trying to implement dynamic module loading functionality for my project. However, I'm failing for past few hours. To give you an idea of what I'm trying to achieve, here is the structure of the project
plugins
developer
assets
scss
developer.scss
js
developer.js
themes
theme_name
webpack.mix.js
node_modules/
source
js
application.js
bootstrap.js
scss
application.scss
_variables.scss
So, in order to get the available plugins, I've made the following function
/**
* Get all plugins for specified developer
* which have 'assets' folder
* #param developerPath
* #param plugins
*/
function getDeveloperPlugins(developerPath, plugins) {
if (fs.existsSync(developerPath)) {
fs.readdirSync(developerPath).forEach(entry => {
let pluginPath = path.resolve(developerPath, entry),
assetsPath = path.resolve(pluginPath, 'assets');
if (fs.existsSync(assetsPath))
plugins[entry] = assetsPath;
});
}
}
This function loads all the available plugins for the specified developer, then goes inside and looks for the assets folder, if it exists, then it returns it and we can work with the provided directory later.
The next step is to generate the reference for every plugin (direct path to the developer_name.js file) which later should be 'mixed' into one plugins.bundle.js file.
In order to achieve this, the following piece of code 'emerged'
_.forEach(plugins, (directory, plugin) => {
let jsFolder = path.resolve(directory, 'js'),
scssFolder = path.resolve(directory, 'scss');
if (fs.existsSync(jsFolder)) {
webpackModules.push(jsFolder);
let possibleFile = path.resolve(jsFolder, plugin + '.js');
if (fs.existsSync(possibleFile))
pluginsBundle.js[plugin] = possibleFile;
}
if (fs.existsSync(scssFolder)) {
webpackModules.push(scssFolder);
let possibleFile = path.resolve(scssFolder, plugin + '.scss');
if (fs.existsSync(possibleFile))
pluginsBundle.scss[plugin] = possibleFile;
}
});
And the last step before I'm starting to edit the configuration of the Webpack is to get the folders for both scss and js files for all plugins and all developers:
let jsPluginsBundle = _.values(pluginsBundle.js),
scssPluginsBundle = _.values(pluginsBundle.scss);
And here is where the problems start to appear. I've tried many solutions offered either here on GitHub (in respective repositories), but I've failed so many times.
The only error I'm having now is this one:
ERROR in F:/Web/Projects/TestProject/plugins/developer/testplugin/assets/js/testplugin.js
Module build failed: ReferenceError: Unknown plugin "transform-object-rest-spread" specified in "base" at 0, attempted to resolve relative to "F:\\Web\\Projects\\TestProject\\plugins\\developer\\testplugin\\assets\\js"
Yes, i know that webpack.mix.js file should be in the root folder of the project, however, i'm just developing theme, which uses modules developed by other members of the team.
So, idea was to:
Start build process: npm run dev|prod
Load plugins for all needed developers automatically
Use methods and html tags provided by the plugin (it is a mix of PHP for API routing and Vue.js for Components, etc) as follows: <test-component></test-component>
Any help is really appreciated, i just cant get my head around that error. If you need extra information, i'm ready to help since i myself need help to solve this issue =)
Update: The latest Webpack config used by mix.webpackConfig() (still failing though)
let webpackConfiguration = {
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: require.resolve('babel-loader'),
options: {
presets: [
'babel-preset-env'
].map(require.resolve),
plugins: [
'babel-plugin-transform-object-rest-spread'
].map(require.resolve)
}
}
}]
},
resolve: {
modules: webpackModules
}
};
mix.webpackConfig(webpackConfiguration);
And this is the content of the webpackModules variable:
[
'F:\\Web\\Projects\\TestProject\\themes\\testtheme\\node_modules',
'F:\\Web\\Projects\\TestProject\\themes\\testtheme',
'F:\\Web\\Projects\\TestProject\\plugins\\developer\\testplugin\\assets\\js',
'F:\\Web\\Projects\\TestProject\\plugins\\developer\\testplugin\\assets\\scss'
]
Okay, after 7 hours I've decided to try the most obvious method to solve the problem, to create node_modules folder in the root of the project and install laravel-mix there, and it worked like a charm.
Looks like, if it cant find the module in the directory outside the root scope of the Webpack, it will go up the tree to find the node_modules folder.
Developers should allow us to set the root folder for Webpack to fetch all the modules i guess, but well, problem is solved anyways.

Using webpack with an existing requirejs application

I am working with an existing application (canvas-lms) that uses RequireJS in its build system. I'm working on a pseudo-standalone application that plugs into Canvas (a "client_app" in Canvas parlance). This is a fontend-only app that makes API calls back to the host Canvas app. The details aren't terribly important for my question - all a client_app needs to do is have a build script that spits out a JS file in a defined place within the Canvas app tree.
I'm trying to use Webpack to build my app instead of RequireJS. Everything works great if I keep all my dependencies self-contained (e.g. npm-install everything I need); however, Canvas already provides many of these dependencies (e.g. React, jQuery), and in jQuery's case, it provides a patched version that I'd like to use instead. This is where I start to have problems.
Getting React to work was easy; Canvas installs it with bower, so I was able to add an alias in my webpack config to point at it:
alias: {
'react': __dirname + '/vendor/canvas/public/javascripts/bower/react/react-with-addons',
}
(__dirname + /vendor/canvas is a symlink in my application tree to the host Canvas application's tree)
Where I'm having trouble is trying to load the provided copy of jQuery.
Canvas has the following jQuery structure:
/public/javascripts/jquery.js:
define(['jquery.instructure_jquery_patches'], function($) {
return $;
});
/public/javascripts/jquery.instructure_jquery_patches.js:
define(['vendor/jquery-1.7.2', 'vendor/jquery.cookie'], function($) {
// does a few things to patch jquery ...
// ...
return $;
});
/public/javascripts/vendor/jquery.cookie.js -- looks like the standard jquery.cookie plugin, wrapped in an AMD define:
define(['vendor/jquery-1.7.2'], function(jQuery) {
jQuery.cookie = function(name, value, options) {
//......
};
});
and finally, /public/javascripts/vendor/jquery-1.7.2.js. Not going to paste it in, since it's bog-standard jQuery1.7.2, except that the AMD define has been made anonymous -- reverting it to the stock behaviour doesn't make a difference.
I want to be able to do something like var $ = require('jquery') or import $ from 'jquery' and have webpack do whatever magic, poorly-documented voodoo it needs to do to use jquery.instructure-jquery-patches.
I've tried adding the path to resolve.root in my webpack.config.js file:
resolve: {
extensions: ['', '.js', '.jsx'],
root: [
__dirname + '/src/js',
__dirname + '/vendor/canvas/public/javascripts'
],
alias: {
'react': 'react/addons',
'react/addons/lib': 'react/../lib'
}
},
This should mean that when I do a require('jquery'), it first finds /public/javascripts/jquery.js, which defines a module with instructure_jquery_patches as a dependency. That falls into instructure_jquery_patches, which defines a module with two dependencies ('vendor/jquery-1.7.2', 'vendor/jquery.cookie').
In my main entry point (index.js), I am importing jQuery (also tried a commonjs require, no difference), and trying to use it:
import React from 'react';
import $ from 'jquery';
$('h1').addClass('foo');
if (__DEV__) {
require('../scss/main.scss');
window.React = window.React || React;
console.log('React: ', React.version);
console.log('jQuery:', $.fn.jquery);
}
Building the bundle with webpack seems to work; there are no errors. When I try to load the page in the browser, though, I'm getting an error from within jquery.instructure-jquery-patches.js:
It would seem that jQuery is not availble within jquery.instructure-jquery-patches.
It is, however, available in the global scope after the page loads, so jQuery is being evaluated and executed.
My guess is that I'm running into some sort of requirejs/amd asynchronicity problem, but that's a shot in the dark. I don't know enough about requirejs or amd to know for sure.
TobiasK's comment pointed me at needing to add amd: { jQuery: true } to my webpack config. Everything is working now.

Categories

Resources