Parcel.js: moving static resources to a separate folder

Paolo Mioni
Jun 10 · 7 min read

Being somewhat tired of configuring different versions of Webpack for different projects and perusing lengthy documentation, searching for plugins and looking at hundreds tutorials, many of which are related to an unspecified version of Webpack which is usually not compatible with the one we’re using, I tried to use Parcel.js for a few new front-end projects.

The prospect of using a “zero configuration web application bundler” was indeed very enticing. In some respects, Parcel.js delivers on that promise, since if your workflow is fairly straightforward it is very quick to install and it works sort of out of the box. In other respects, I found out that this comes at a cost. If you workflow doesn’t match the use cases that have been foreseen by the developers, you will need to add configuration to your project for the various plug-ins it uses (PostCSS, Sass, etc.). But more on that in a future article.

The problem

One of the big drawbacks I’ve found with Parcel.js is that all resources are output in the same folder. Parcel.js avoids naming conflicts in your assets by hashing all files it produces with a hash which (I believe) changes every time the content changes size, so it provides an automatic cache busting system when you deploy a new version of your site/application to a server. Which is nice. However, if your initial asset structure looks something like this:

Original assets folder, with fonts and images in separate folders and subfolders

once you build it for production it will be converted into something like this:

Flattened folder in build, with all resources on the same level

all files are on the same level, and if you have many files in your project you end up with a huge folder full of hashed files, whose names are also fairly difficult to read.

Which is all fine, if the only thing you need to do with your built application is to push it to production using a Continuous Integration tool that will build and deploy for you. However, there are some use cases in which this won’t work. Some that come to my mind are the following:

  1. If you need to setup some specific server-side rules for some assets only (for example, some JPEGs but not all of them, or some fonts but not all of them). In this case it might be much more convenient to create a per-folder rule than to use some complicated naming convention for each file and use RegExps to match the files you need. It would be helpful, and maybe simple to implement, if Parcel.js would at least prepend each file’s current folder hierarchy on the name of the file, which would make it much easier to create said RegExps.
  2. If you ever need to look at the files in the build, maybe to look for something problematic in a file’s compression, for example, once an application becomes big it becomes very difficult to find what you’re looking for.
  3. It might not be a problem anymore, and it might depend on the file system you are using in production, but sometimes there are limits on how many files you can have in one folder, and having too many files in a folder has a performance hit. Parcel’s own build cache system uses several different folders to cache intermediate compilation artifacts, for the same reason.
  4. You might need to deliver the built package to a third party for integration, for example if you’re only developing the front-end of an application or site, which then needs to be integrated with a CMS of some sort. Go tell the implementers that they need to fetch all their templates for, say, twenty layouts or fifty components from the root folder, and that all of the resources and images, Javascript, compiled CSS and all, are in the same folder as well. It just doesn’t look right (or professional, for that matter).

My use case was number four. The implementers did not want anything to do with our front-end build process, and did not even want to look at the components we’d created for the project (which in our case were Handlebars partials). They just wanted the plain HTML and the compiled JavaScript and CSS, possibly with a diff of changes to the HTML when we delivered a new package. It sounds primitive, and all evidence points to the fact that these implementers are probably coding in Dreamweaver or FrontPage or something, but these things do happen in real life, even with big companies. I haven’t had the courage to ask if they were doing the site in Joomla.

I searched Parcel’s documentation to find some option that would do that, but in vain. I looked in the plethora of Parcel open issues, and found a few threads started by people with the same problem, one of them being this one, but there are several. Apparently, this feature will be available in the next version, Parcel 2, which is still in development.

I had a look at the code to see if it was feasible, given my time constraints with the project I was working on, to create a plugin to do what I wanted. But that would have required me to study a big part of Parcel’s source code, and it was a total no-go for me. So I decided to build a custom script to achieve what I needed, after Parcel’s build process was finished.

The solution

It is actually a very simple script, however it has worked for me for the project I needed it for, and it has in a couple more project I did with Parcel after that one. So I decided to share it with you, while we all wait that the guys at Parcel deliver version 2. Here is the code, from a file I called postbuild.js. It is in Node.js, it is NOT very efficient, it does NOT run in parallel, but it gets the job done:

const fs = require('fs');
const path = require('path');
const replace = require('replace-in-file');
const escapeRegExp = require('lodash.escaperegexp');

// the directory in which you're outputting your build
let baseDir = 'public'
// the name for the directory where your static files will be moved to
let staticDir = 'static'
// the directory where your built files (css and JavaScript) will be moved to
let assetsDir = 'build'

// if the staticDir directory isn't there, create it
if (!fs.existsSync(path.join(__dirname, baseDir, staticDir))){
fs.mkdirSync(path.join(__dirname, baseDir, staticDir));
}

// same for the assetsDir directory
if (!fs.existsSync(path.join(__dirname, baseDir, assetsDir))){
fs.mkdirSync(path.join(__dirname, baseDir, assetsDir));
}

// Loop through the baseDir directory
fs.readdir(`./${baseDir}`, (err, files) => {
// store all files in custom arrays by type
let html = []
let js = []
let css = []
let maps = []
let staticAssets = []

files.forEach(file => {
// first HTML files
if(file.match(/.+\.(html)$/)) {
console.log('html match', file)
html.push(file)
} else if(file.match(/.+\.(js)$/)) { // then JavaScripts
js.push(file)
} else if(file.match(/.+\.(map)$/)) { // then CSS
maps.push(file)
} else if(file.match(/.+\.(css)$/)) { // then sourcemaps
css.push(file)
} else if(file.match(/.+\..+$/)){ // all other files, exclude current directory and directory one level up
staticAssets.push(file)
}
});
// check what went where
console.log('html', html, 'css', css, 'js', js, 'staticAssets', staticAssets)

// create an array for all compiled assets
let assets = css.concat(js).concat(maps)

// replace all other resources in html
html.forEach(
file => {
staticAssets.forEach(name => {
let options = {
files: path.join('public', file),
from: new RegExp(escapeRegExp(name), 'g'),
to: staticDir + '/' + name
}
try {
let changedFiles = replace.sync(options);
console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
console.error('Error occurred:', error);
}
})
assets.forEach(name => {
let options = {
files: path.join('public', file),
from: new RegExp(escapeRegExp(name), 'g'),
to: assetsDir + '/' + name
}
try {
let changedFiles = replace.sync(options);
console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
console.error('Error occurred:', error);
}
})
}
)

// replace map links in js
js.forEach(
file => {
maps.forEach(name => {
let options = {
files: path.join('public', file),
from: name,
to: '../' + assetsDir + '/' + name
}
try {
let changedFiles = replace.sync(options);
console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
console.error('Error occurred:', error);
}
})
}
)

// replace links in css
css.forEach(
file => {
staticAssets.forEach(name => {
let options = {
files: path.join('public', file),
from: new RegExp(escapeRegExp(name), 'g'),
to: '../' + staticDir + '/' + name
}
try {
let changedFiles = replace.sync(options);
console.log('Modified files:', changedFiles.join(', '));
}
catch (error) {
console.error('Error occurred:', error);
}
})
}
)

// move js and css and maps
assets.forEach(
name => {
fs.rename(path.join(__dirname, 'public', name), path.join(__dirname, 'public', assetsDir, name), function (err) {
if (err) throw err
console.log(`Successfully moved ${name}`)
})
}
)
// move staticAssets
staticAssets.forEach(
name => {
fs.rename(path.join(__dirname, 'public', name), path.join(__dirname, 'public', staticDir, name), function (err) {
if (err) throw err
console.log(`Successfully moved ${name}`)
})
}
)


});

You can just copy and paste (and change) this code in your project, save it in the root of your project and call it postbuild.js, then run it after your standard parcel build scripts.

In my package.jsonI use it like this after the build script:

"build": "parcel build pages/index.hbs --out-dir public --no-cache && node postbuild.js"

I decided not to build a NPM package for this, for the following reasons:

  • It is a temporary, less than ideal solution, not worth of being made permanent in any way
  • I would not have the time to maintain a package
  • People should learn that not every problem they have with their build compilation flow can be solved with the magic inclusion of a third-party NPM package. Sometimes it’s best to write some code yourself.

Hope this little thing serves your purpose, if it does let me know in the comments, if it doesn’t, let me know in the comments, if you have something else you want to say about this matter, let me know in the comments.

Thanks for your time.

HCEverything

A blog by Italian web design company HCE.IT. Tech, humour, code and nonsense.

Paolo Mioni

Written by

CTO and co-founder at HCE.IT: lover of front-end development, complex technical problems and noisy electric guitars.

HCEverything

A blog by Italian web design company HCE.IT. Tech, humour, code and nonsense.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade