I have a question concerning best practice for including node_modules
into a HTML website.
Imagine I have Bootstrap inside my node_modules
folder. Now for the production version of the website, how would I include the Bootstrap script and CSS files located inside the node_modules
folder? Does it make sense to leave Bootstrap inside that folder and do something like the following?
<script src="./node_modules/bootstrap/dist/bootstrap.min.js"></script>
Or would I have to add rules to my gulp file which then copy those files into my dist folder? Or would it be best to let gulp somehow completely remove the local bootstrap from my HTML file and replace it with the CDN version?
Usually, you don't want to expose any of your internal paths for how your server is structured to the outside world. What you can is make a /scripts
static route in your server that fetches its files from whatever directory they happen to reside in. So, if your files are in "./node_modules/bootstrap/dist/"
. Then, the script tag in your pages just looks like this:
<script src="/scripts/bootstrap.min.js"></script>
If you were using express with nodejs, a static route is as simple as this:
app.use('/scripts', express.static(__dirname + '/node_modules/bootstrap/dist/'));
Then, any browser requests from /scripts/xxx.js
will automatically be fetched from your dist
directory at __dirname + /node_modules/bootstrap/dist/xxx.js
.
Note: Newer versions of NPM put more things at the top level, not nested so deep so if you are using a newer version of NPM, then the path names will be different than indicated in the OP's question and in the current answer. But, the concept is still the same. You find out where the files are physically located on your server drive and you make an app.use()
with express.static()
to make a pseudo-path to those files so you aren't exposing the actual server file system organization to the client.
If you don't want to make a static route like this, then you're probably better off just copying the public scripts to a path that your web server does treat as /scripts
or whatever top level designation you want to use. Usually, you can make this copying part of your build/deployment process.
If you want to make just one particular file public in a directory and not everything found in that directory with it, then you can manually create individual routes for each file rather than use express.static()
such as:
<script src="/bootstrap.min.js"></script>
And the code to create a route for that
app.get('/bootstrap.min.js', function(req, res) {
res.sendFile(__dirname + '/node_modules/bootstrap/dist/bootstrap.min.js');
});
Or, if you want to still delineate routes for scripts with /scripts
, you could do this:
<script src="/scripts/bootstrap.min.js"></script>
And the code to create a route for that
app.get('/scripts/bootstrap.min.js', function(req, res) {
res.sendFile(__dirname + '/node_modules/bootstrap/dist/bootstrap.min.js');
});
I would use the path npm module and then do something like this:
var path = require('path');
app.use('/scripts', express.static(path.join(__dirname, 'node_modules/bootstrap/dist')));
IMPORTANT: we use path.join to make paths joining using system agnostic way, i.e. on windows and unix we have different path separators (/ and )
node_modules/bootstrap/dist
and instead use /
?
As mentioned by jfriend00 you should not expose your server structure. You could copy your project dependency files to something like public/scripts
. You can do this very easily with dep-linker like this:
var DepLinker = require('dep-linker');
DepLinker.copyDependenciesTo('./public/scripts')
// Done
src
s will then be something like /scripts/[package-name]/dist/file.min.js
.
If you want a quick and easy solution (and you have gulp installed).
In my gulpfile.js
I run a simple copy paste task that puts any files I might need into ./public/modules/
directory.
gulp.task('modules', function() {
sources = [
'./node_modules/prismjs/prism.js',
'./node_modules/prismjs/themes/prism-dark.css',
]
gulp.src( sources ).pipe(gulp.dest('./public/modules/'));
});
gulp.task('copy-modules', ['modules']);
The downside to this is that it isn't automated. However, if all you need is a few scripts and styles copied over (and kept in a list), this should do the job.
'scripts': {'install':'yarn gulp copy-modules'}
, assuming yarn is the package manager and gulp is installed in package.
The directory 'node_modules' may not be in current directory, so you should resolve the path dynamically.
var bootstrap_dir = require.resolve('bootstrap')
.match(/.*\/node_modules\/[^/]+\//)[0];
app.use('/scripts', express.static(bootstrap_dir + 'dist/'));
This is what I have setup on my express
server:
// app.js
const path = require('path');
const express = require('express');
const expressApp = express();
const nm_dependencies = ['bootstrap', 'jquery', 'popper.js']; // keep adding required node_modules to this array.
nm_dependencies.forEach(dep => {
expressApp.use(`/${dep}`, express.static(path.resolve(`node_modules/${dep}`)));
});
<!-- somewhere inside head tag -->
<link rel="stylesheet" href="bootstrap/dist/css/bootstrap.css" />
<!-- somewhere near ending body tag -->
<script src="jquery/dist/jquery.js" charset="utf-8"></script>
<script src="popper.js/dist/popper.js" charset="utf-8"></script>
<script src="bootstrap/dist/js/bootstrap.js" charset="utf-8"></script>
Good Luck...
I didn't find any clean solutions (I don't want to expose the source of all my node_modules) so I just wrote a Powershell script to copy them:
$deps = "leaflet", "leaflet-search", "material-components-web"
foreach ($dep in $deps) {
Copy-Item "node_modules/$dep/dist" "static/$dep" -Recurse
}
I want to update this question with an easier solution. Create a symbolic link to node_modules.
The easiest way to grant public access to node_modules is to create a symbolic link pointing to your node_modules from within your public directory. The symlink will make it as if the files exist wherever the link is created.
For example, if the node server has code for serving static files
app.use(serveStatic(path.join(__dirname, 'dist')));
and __dirname refers to /path/to/app so that your static files are served from /path/to/app/dist
and node_modules is at /path/to/app/node_modules, then create a symlink like this on mac/linux:
ln -s /path/to/app/node_modules /path/to/app/dist/node_modules
or like this on windows:
mklink /path/to/app/node_modules /path/to/app/dist/node_modules
Now a get request for:
node_modules/some/path
will receive a response with the file at
/path/to/app/dist/node_modules/some/path
which is really the file at
/path/to/app/node_modules/some/path
If your directory at /path/to/app/dist is not a safe location, perhaps because of interference from a build process with gulp or grunt, then you could add a separate directory for the link and add a new serveStatic call such as:
ln -s /path/to/app/node_modules /path/to/app/newDirectoryName/node_modules
and in node add:
app.use(serveStatic(path.join(__dirname, 'newDirectoryName')));
I did the below changes to AUTO-INCLUDE the files in the index html. So that when you add a file in the folder it will automatically be picked up from the folder, without you having to include the file in index.html
//// THIS WORKS FOR ME
///// in app.js or server.js
var app = express();
app.use("/", express.static(__dirname));
var fs = require("fs"),
function getFiles (dir, files_){
files_ = files_ || [];
var files = fs.readdirSync(dir);
for (var i in files){
var name = dir + '/' + files[i];
if (fs.statSync(name).isDirectory()){
getFiles(name, files_);
} else {
files_.push(name);
}
}
return files_;
}
//// send the files in js folder as variable/array
ejs = require('ejs');
res.render('index', {
'something':'something'...........
jsfiles: jsfiles,
});
///--------------------------------------------------
///////// in views/index.ejs --- the below code will list the files in index.ejs
<% for(var i=0; i < jsfiles.length; i++) { %>
<script src="<%= jsfiles[i] %>"></script>
<% } %>
To use multiple files from node_modules in html, the best way I've found is to put them to an array and then loop on them to make them visible for web clients, for example to use filepond modules from node_modules:
const filePondModules = ['filepond-plugin-file-encode', 'filepond-plugin-image-preview', 'filepond-plugin-image-resize', 'filepond']
filePondModules.forEach(currentModule => {
let module_dir = require.resolve(currentModule)
.match(/.*\/node_modules\/[^/]+\//)[0];
app.use('/' + currentModule, express.static(module_dir + 'dist/'));
})
And then in the html (or layout) file, just call them like this :
<link rel="stylesheet" href="/filepond/filepond.css">
<link rel="stylesheet" href="/filepond-plugin-image-preview/filepond-plugin-image-preview.css">
...
<script src="/filepond-plugin-image-preview/filepond-plugin-image-preview.js" ></script>
<script src="/filepond-plugin-file-encode/filepond-plugin-file-encode.js"></script>
<script src="/filepond-plugin-image-resize/filepond-plugin-image-resize.js"></script>
<script src="/filepond/filepond.js"></script>
If you are linking to many files, create a whitelist, and then use sendFile():
app.get('/npm/:pkg/:file', (req, res) => {
const ok = ['jquery','bootstrap','interactjs'];
if (!ok.includes(req.params.pkg)) res.status(503).send("Not Permitted.");
res.sendFile(__dirname + `/node_modules/${req.params.pkg}/dist/${req.params.file}`);
});
For example, You can then safely link to /npm/bootstrap/bootsrap.js, /npm/bootstrap/bootsrap.css, etc.
As an aside, I would love to know if there was a way to whitelist using express.static
Success story sharing
/scripts
because that way any dependencies inside the module would be maintained.express.static()
sends the first match it finds. Since eachexpress.static()
statement only creates one possible matching path, there should not be duplicates from oneexpress.static()
statement. If you have multipleexpress.static()
statements that each could have a match, then the first one in your code is the one whose match will be used. Also, you really shouldn't have multiple files with the same name and relative path reachable byexpress.static()
statements. Best practice is to not do that.express.static()
at a directory that ONLY contains public files (include sub-directories). So, I can't really imagine a project where there were lots ofdist
directories being published. That seems very unusual to me. Perhaps you want to make a specific route to a specific file or to 2-3 files. In that case, you are responsible for disambiguating those files. It's not going to do you any good to have fourscript.js
files anyway./script.js
is requested by the browser. So, the abstract answer is that you should not allow the situation of duplicate filenames to exist because it's not a solvable problem unless you require a unique prefix for each directory of files. Then, duplicate root names are not a problem anyway. For a more specific answer with a detailed recommendation, you would need to post your own question with exact details.