In a current webpack project my code is partitioned into modules, each which has a main entry point with a .module.js ending. E.g. for the module Util, the entry point is util.module.js and I include it in other modules by writing import util from 'modules/util/util.module.js'
However, since all modules have the same syntax for the entry point, I would rather avoid specifying the full filename of the entry. E.g, it would be nice if I could just write import util from 'modules/util'.
I know this works if util.module.js is name index.js, but how can I tell Webpack to pick up my current module entries in the same way?
I'll attempt to outline some readily available approaches, starting from your requirements and progressing towards something that's more conventional.
I sense that some overengineering of build tools might be creeping in, so I'd highly recommend you think again whether all of this is really necessary and reconsider the conventional approach.
It is analogous to how others would expect modules get to resolved, does not require Webpack and will not break other tools that internally attempt to resolve modules according to this strategy, like eslint-plugin-import.
NormalModuleReplacementPlugin
Since you need to include the directory name in the module you're trying to resolve, you can use the NormalModuleReplacementPlugin to construct the resolve path dynamically:
new webpack.NormalModuleReplacementPlugin(/modules\/(\w+)$/, (result) => {
result.request += `/${result.request.split('/').pop()}.module.js`
}
You'd need to play around with this to ensure all valid names are covered.
resolve.extensions
If you really want to somehow distinguish these modules, then you could do so by providing a specific extension for them:
modules/util/index.module.js
modules/dashboard/index.module.js
It would be enough to configure:
{
resolve: {
extensions: ['.js', '.module.js']
}
}
Package main
Another approach would be to extract all your modules as packages, specifying {main} in package.json.
This will set you on a path towards a monorepository.
Conventional
The conventional approach would be to name the modules index.js:
modules/util/index.js
modules/dashboard/index.js
Related
I've got a Next.js project configured to resolve imports that end in .web.js. This works outside of my node_modules directory. I did this by setting resolve.extensions = ['.web.js, '.js', '.jsx'] in my webpack config. I understand that this setting is responsible for resolving imports that don't have an extension, e.g. import _ from './component', when ./component.web.js exists.
I also have some node_modules that make use of this .web.js extension. They're private modules, but the idea stands. Let's say our node_modules looks like this. It may be worth noting that these modules have already been transpiled and as such use require rather than import.
- node_modules
- #foo
- bar.js
- baz.web.js
- baz.native.js
Now let's say that we have the following:
// bar.js
require("./baz");
If I try to import #foo/bar, the app will throw a module not found error on the line require("./baz"); saying that it can't be found. If I change it to require("./baz.web.js") or remove the line altogether then the app runs fine.
Why can webpack make these kind of resolutions outside of node_modules, but not within the directory? And how can I tell webpack to resolve those imports, too?
Depending on your module resolution strategy, you'll either find some files or not. Node.js resolves modules as outlined here. This means for you that require('./baz') is resolved to requesting /path/to/module/baz.js. Since your file is not actually named, it is not found. You can use require('./baz.web') instead.
As to whether Webpack can "automatically" handle which import to use, it probably comes down to using a plugin or having some sort of logic in bar.js to choose between baz.web and baz.native.
This is just something I thought today and I didn't see a lot of information so I'm going to share this weird cases and how I personally solved them (if there's a better way please comment, but meanwhile this might help others ^^)
In a regular module, you would do something like this to export your function/library/object/data:
// regular NodeJS way:
module.exports = data;
// ES6 way
// (will get transpiled to the regular way using the module variable by webpack)
export data;
default export data;
When compiling the library usually babel or tsc are used, but if for any reason you want not only to compile (transpile) your library but also pack it using webpack, you will encounter this case.
As you know, in a webpack bundle the module variable is local to the bundle (every module/file gets wrapped with a function where module is a parameter = local variable), so nothing really gets exported outside the bundle, is just nicely managed by webpack.
That means that you can't also access the contents using the regular require/import methods.
In some case you might find necessary to export outside webpack. (i.e. you are trying to build a library using webpack and you want it to be accessible by other people). This basically means you need to access the original module variable, but webpack doesn't expose it like it happened with __non_webpack_require__.
See also: Importing runtime modules from outside webpack bundle
The solution is to create our own __non_webpack_module__ (as webpack does with __non_webpack_require__.
How I did it is using webpack.BannerPlugin to inject some code outside the bundle. This code is prepended to the build after the minification is done, so it's preserved safely.
In your webpack.config.js:
plugins: [
new BannerPlugin({
raw: true,
banner: `const __non_webpack_module__ = module;`,
}),
]
And again, if you are using TypeScript, in global.d.ts:
declare const __non_webpack_module__: NodeModule;
And now, you can do something like this in your code:
__non_webpack_module__.exports = /* your class/function/data/whatever */
This will allow to import it as usual from other files
Tip: You might want to look at BannerPlugin to check other options, like include or exclude so this variable is only generated on the desired files, etc.
I'm developing a module that doesn't have a build that the user imports. Instead, he imports individual components and then bundles them along with his code. However, those components share utilities and I want to import them without going through relative path hell.
I know that's a pretty common question and I did some research. Suppose I have module/components/foo/bar/baz/index.js that wants to import module/utils/helper.js
Option 1
Just use relative paths and do:
import helper from '../../../../utils/helper'
Option 2
Use the module-alias package and have:
import helper from '#utils/helper'
This would work in Node.js because modules are resolved at runtime. However, let's say the module user has Webpack and imports the module:
import component from 'module/components/foo/bar/baz'
Webpack wouldn't be able to resolve #utils unless the user specifies that alias in his own Webpack configuration. That would be pretty annoying.
Option 3
Use Webpack aliases in webpack.config.js:
module.exports = {
resolve: {
alias: {
'#utils': path.join(__dirname, 'utils')
}
}
}
This would work fine if the module was pre-bundled. But as I've previously mentioned, I want the library to be usable with ES6 imports so that users can bundle only the parts they need.
Option 4
I could use the module name in the module's own source code:
import helper from 'module/utils/helper'
This appears to solve the problem, but I think it's a pretty bad solution. For development, you'd have to create a symlink node_modules/module -> module. I'm sure this hides many potential issues and collaborators would have to manually do it as well.
Is there a way to avoid relative paths while allowing the library to be used with ES6 imports?
I'm trying to follow the Jest getting started guide but with ES6 Modules and Babel.My root folder has two javascript files sumfn.js and sum.test.js. My sum.test.js file looks like this:
import { sum } from 'sumfn';
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3);
});
However it seems like Jest is having trouble resolving sumfn, even though it clearly does find the file sunfn.js.
● Test suite failed to run
Cannot find module 'sumfn' from 'sum.test.js'
However, Jest was able to find:
'./sumfn.js'
You might want to include a file extension in your import, or update your 'moduleFileExtensions', which is currently ['js', 'json', 'jsx', 'ts', 'tsx', 'node'].
If I change the import line to use ./sumfn, it works. However, from what I read about ES6 imports, it seems like it should be able to find files in the same directory. Is this not supported in Jest perhaps?
You need to add the ./ to tell JavaScript The module you're looking for can be found in the following file path, relative to the current file. So you need to add ./ to say Look for sumfn.js in the current directory.
So one of the main reasons why I was confused is because there are a lot of out-dated ES6 module guides out there. In fact, many of the top results on Google seem to be outdated. I was looking at guides like this, this, and this, and they all said that you can import from a module name, without specifying the path, eg
import { double } from 'mymodule';
These are called "bare" import specifiers, and the guides said that it will by default search the current directory for a matching module. However, it seems like right now they are not supported in browsers.
Where it gets extremely confusing is that they are supported in BabelJS and Webpack, but it follows a different convention. For example, Webpack searches the paths specified in resolve.modules, which by default includes the node_modules folder. This is why the Create-React-App example can use statements like
import React from 'react';
It seems like the plan going forward is to let the environment determine how to resolve these "bare" specifiers (Source). Seems dangerous to let every environment resolve these specifiers differently, which could make it hard to make cross-compatible modules, but I guess that's the current plan for now.
I'm new to RequireJS. I understand it for the most part. However, the r.js optimization process confuses me. Two questions:
Doesn't concatenating all source into a single file defeat the purpose of RequireJS's lazy-loading abilities?
If I do optimize using r.js and have everything in a single file, do I then have to manually update the path info in the config to point to that single file? And do the dependencies I've defined as individual modules have to now be changed throughout the entire application to point to this single file? Here's just a pretend source to illustrate how I'm currently setup:
requirejs.config({
paths : {
mod1 : 'app/common/module1',
mod2 : 'app/common/module2',
mod3 : 'app/common/module3',
},
});
-- MOD 1
define(["mod2", "mod3"], function(mod2, mod3) {
// do something
}
Does that now have to be manually updated after optimization to look like this?
requirejs.config({
paths : {
optimizedMod : 'build-dir/optimizedModule',
},
});
-- MOD 1
define(["optimizedMod"], function(optimizedMod) {
// do something
}
Re. 1. No, it doesn't. r.js analyzes your dependency tree and (by default) only includes modules you'd need to load on application startup anyway. The dependencies that are required dynamically won't be included, they'll be lazy-loaded at runtime (unless you set findNestedDependencies to true).
However, lazy-loading is arguably not the main benefit of using RequireJS, a bigger thing is modularisation itself. Being forced to manage dependencies makes it harder to write code that's not testable or refactorable - bad architecture can be immediatelly spotted (lengthy dependencies lists, "god" modules, etc.)
Re. 2. This is precisely the reason why you shouldn't be naming your own modules or mapping them in the paths configuration element. paths should be used for third party libraries and not your own code, centralising name->path mappings reduces flexibility. Once you refer to dependencies via their paths (relative to baseUrl) r.js can rewrite them at build time:
define(["app/common/module2", "app/common/module3"], function(mod2, mod3) {
// do something
}